├── services ├── author │ ├── .gitignore │ ├── src │ │ ├── utils │ │ │ ├── db.ts │ │ │ ├── dataUri.ts │ │ │ ├── TryCatch.ts │ │ │ └── rabbitmq.ts │ │ ├── middlewares │ │ │ ├── multer.ts │ │ │ └── isAuth.ts │ │ ├── routes │ │ │ └── blog.ts │ │ ├── server.ts │ │ └── controllers │ │ │ └── blog.ts │ ├── tsconfig.json │ └── package.json ├── blog │ ├── .gitignore │ ├── src │ │ ├── utils │ │ │ ├── db.ts │ │ │ ├── TryCatch.ts │ │ │ └── consumer.ts │ │ ├── routes │ │ │ └── blog.ts │ │ ├── server.ts │ │ ├── middleware │ │ │ └── isAuth.ts │ │ └── controllers │ │ │ └── blog.ts │ ├── tsconfig.json │ └── package.json └── user │ ├── .gitignore │ ├── src │ ├── middleware │ │ ├── multer.ts │ │ └── isAuth.ts │ ├── utils │ │ ├── dataUri.ts │ │ ├── db.ts │ │ ├── GoogleConfig.ts │ │ └── TryCatch.ts │ ├── routes │ │ └── user.ts │ ├── server.ts │ ├── model │ │ └── User.ts │ └── controllers │ │ └── user.ts │ ├── tsconfig.json │ └── package.json └── frontend ├── postcss.config.mjs ├── public ├── google.png ├── vercel.svg ├── window.svg ├── file.svg ├── globe.svg └── next.svg ├── src ├── app │ ├── favicon.ico │ ├── page.tsx │ ├── blogs │ │ ├── layout.tsx │ │ └── page.tsx │ ├── layout.tsx │ ├── blog │ │ ├── saved │ │ │ └── page.tsx │ │ ├── edit │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ ├── new │ │ │ └── page.tsx │ │ └── [id] │ │ │ └── page.tsx │ ├── login │ │ └── page.tsx │ ├── profile │ │ ├── [id] │ │ │ └── page.tsx │ │ └── page.tsx │ └── globals.css ├── lib │ └── utils.ts ├── components │ ├── loading.tsx │ ├── ui │ │ ├── skeleton.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── input.tsx │ │ ├── avatar.tsx │ │ ├── tooltip.tsx │ │ ├── card.tsx │ │ ├── button.tsx │ │ ├── dialog.tsx │ │ ├── sheet.tsx │ │ ├── select.tsx │ │ └── sidebar.tsx │ ├── BlogCard.tsx │ ├── sidebar.tsx │ └── navbar.tsx ├── hooks │ └── use-mobile.ts └── context │ └── AppContext.tsx ├── next.config.ts ├── eslint.config.mjs ├── components.json ├── .gitignore ├── tsconfig.json ├── package.json └── README.md /services/author/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | .env 4 | Dockerfile 5 | .dockerignore -------------------------------------------------------------------------------- /services/blog/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | .env 4 | Dockerfile 5 | .dockerignore -------------------------------------------------------------------------------- /services/user/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | .env 4 | Dockerfile 5 | .dockerignore -------------------------------------------------------------------------------- /frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /frontend/public/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meprashantkumar/blog-microservice-project-2025/HEAD/frontend/public/google.png -------------------------------------------------------------------------------- /frontend/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meprashantkumar/blog-microservice-project-2025/HEAD/frontend/src/app/favicon.ico -------------------------------------------------------------------------------- /frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import React from "react"; 3 | 4 | const Home = () => { 5 | return redirect("/blogs"); 6 | }; 7 | 8 | export default Home; 9 | -------------------------------------------------------------------------------- /services/author/src/utils/db.ts: -------------------------------------------------------------------------------- 1 | import { neon } from "@neondatabase/serverless"; 2 | import dotenv from "dotenv"; 3 | 4 | dotenv.config(); 5 | 6 | export const sql = neon(process.env.DB_URL as string); 7 | -------------------------------------------------------------------------------- /services/blog/src/utils/db.ts: -------------------------------------------------------------------------------- 1 | import { neon } from "@neondatabase/serverless"; 2 | import dotenv from "dotenv"; 3 | 4 | dotenv.config(); 5 | 6 | export const sql = neon(process.env.DB_URL as string); 7 | -------------------------------------------------------------------------------- /frontend/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /services/author/src/middlewares/multer.ts: -------------------------------------------------------------------------------- 1 | import multer from "multer"; 2 | 3 | const storage = multer.memoryStorage(); 4 | 5 | const uploadFile = multer({ storage }).single("file"); 6 | 7 | export default uploadFile; 8 | -------------------------------------------------------------------------------- /services/user/src/middleware/multer.ts: -------------------------------------------------------------------------------- 1 | import multer from "multer"; 2 | 3 | const storage = multer.memoryStorage(); 4 | 5 | const uploadFile = multer({ storage }).single("file"); 6 | 7 | export default uploadFile; 8 | -------------------------------------------------------------------------------- /frontend/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | eslint: { 6 | ignoreDuringBuilds: true, 7 | }, 8 | }; 9 | 10 | export default nextConfig; 11 | -------------------------------------------------------------------------------- /frontend/src/components/loading.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Loading = () => { 4 | return ( 5 |
6 |

Loading...

7 |
8 | ); 9 | }; 10 | 11 | export default Loading; 12 | -------------------------------------------------------------------------------- /frontend/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ) 11 | } 12 | 13 | export { Skeleton } 14 | -------------------------------------------------------------------------------- /services/author/src/utils/dataUri.ts: -------------------------------------------------------------------------------- 1 | import DataUriParser from "datauri/parser.js"; 2 | import path from "path"; 3 | 4 | const getBuffer = (file: any) => { 5 | const parser = new DataUriParser(); 6 | 7 | const extName = path.extname(file.originalname).toString(); 8 | 9 | return parser.format(extName, file.buffer); 10 | }; 11 | 12 | export default getBuffer; 13 | -------------------------------------------------------------------------------- /services/user/src/utils/dataUri.ts: -------------------------------------------------------------------------------- 1 | import DataUriParser from "datauri/parser.js"; 2 | import path from "path"; 3 | 4 | const getBuffer = (file: any) => { 5 | const parser = new DataUriParser(); 6 | 7 | const extName = path.extname(file.originalname).toString(); 8 | 9 | return parser.format(extName, file.buffer); 10 | }; 11 | 12 | export default getBuffer; 13 | -------------------------------------------------------------------------------- /services/user/src/utils/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const connectDb = async () => { 4 | try { 5 | mongoose.connect(process.env.MONGO_URI as string, { 6 | dbName: "blog", 7 | }); 8 | 9 | console.log("Connected to mongodb"); 10 | } catch (error) { 11 | console.log(error); 12 | } 13 | }; 14 | 15 | export default connectDb; 16 | -------------------------------------------------------------------------------- /frontend/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /services/blog/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "NodeNext", 5 | "rootDir": "./src", 6 | "outDir": "./dist", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "skipLibCheck": true 11 | }, 12 | "include": ["src/**/*"], 13 | "exclude": ["dist", "node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /services/user/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "NodeNext", 5 | "rootDir": "./src", 6 | "outDir": "./dist", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "skipLibCheck": true 11 | }, 12 | "include": ["src/**/*"], 13 | "exclude": ["dist", "node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /frontend/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /services/author/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "NodeNext", 5 | "rootDir": "./src", 6 | "outDir": "./dist", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "skipLibCheck": true 11 | }, 12 | "include": ["src/**/*"], 13 | "exclude": ["dist", "node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /services/user/src/utils/GoogleConfig.ts: -------------------------------------------------------------------------------- 1 | import { google } from "googleapis"; 2 | import dotenv from "dotenv"; 3 | 4 | dotenv.config(); 5 | 6 | const GOOGLE_CLIENT_ID = process.env.Google_Client_id; 7 | const GOOGLE_CLIENT_SECRET = process.env.Google_client_secret; 8 | 9 | export const oauth2client = new google.auth.OAuth2( 10 | GOOGLE_CLIENT_ID, 11 | GOOGLE_CLIENT_SECRET, 12 | "postmessage" 13 | ); 14 | -------------------------------------------------------------------------------- /frontend/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /services/author/src/utils/TryCatch.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, RequestHandler, Response } from "express"; 2 | 3 | const TryCatch = (handler: RequestHandler): RequestHandler => { 4 | return async (req: Request, res: Response, next: NextFunction) => { 5 | try { 6 | await handler(req, res, next); 7 | } catch (error: any) { 8 | res.status(500).json({ 9 | message: error.message, 10 | }); 11 | } 12 | }; 13 | }; 14 | 15 | export default TryCatch; 16 | -------------------------------------------------------------------------------- /services/blog/src/utils/TryCatch.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, RequestHandler, Response } from "express"; 2 | 3 | const TryCatch = (handler: RequestHandler): RequestHandler => { 4 | return async (req: Request, res: Response, next: NextFunction) => { 5 | try { 6 | await handler(req, res, next); 7 | } catch (error: any) { 8 | res.status(500).json({ 9 | message: error.message, 10 | }); 11 | } 12 | }; 13 | }; 14 | 15 | export default TryCatch; 16 | -------------------------------------------------------------------------------- /services/user/src/utils/TryCatch.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, RequestHandler, Response } from "express"; 2 | 3 | const TryCatch = (handler: RequestHandler): RequestHandler => { 4 | return async (req: Request, res: Response, next: NextFunction) => { 5 | try { 6 | await handler(req, res, next); 7 | } catch (error: any) { 8 | res.status(500).json({ 9 | message: error.message, 10 | }); 11 | } 12 | }; 13 | }; 14 | 15 | export default TryCatch; 16 | -------------------------------------------------------------------------------- /frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /frontend/src/app/blogs/layout.tsx: -------------------------------------------------------------------------------- 1 | import SideBar from "@/components/sidebar"; 2 | import { SidebarProvider } from "@/components/ui/sidebar"; 3 | import React, { ReactNode } from "react"; 4 | 5 | interface BlogsProps { 6 | children: ReactNode; 7 | } 8 | 9 | const HomeLayout: React.FC = ({ children }) => { 10 | return ( 11 |
12 | 13 | 14 |
15 |
{children}
16 |
17 |
18 |
19 | ); 20 | }; 21 | 22 | export default HomeLayout; 23 | -------------------------------------------------------------------------------- /services/user/src/routes/user.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | getUserProfile, 4 | loginUser, 5 | myProfile, 6 | updateProfilePic, 7 | updateUser, 8 | } from "../controllers/user.js"; 9 | import { isAuth } from "../middleware/isAuth.js"; 10 | import uploadFile from "../middleware/multer.js"; 11 | 12 | const router = express.Router(); 13 | 14 | router.post("/login", loginUser); 15 | router.get("/me", isAuth, myProfile); 16 | router.get("/user/:id", getUserProfile); 17 | router.post("/user/update", isAuth, updateUser); 18 | router.post("/user/update/pic", isAuth, uploadFile, updateProfilePic); 19 | 20 | export default router; 21 | -------------------------------------------------------------------------------- /frontend/src/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /frontend/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import "./globals.css"; 3 | import Navbar from "@/components/navbar"; 4 | import { AppProvider } from "@/context/AppContext"; 5 | import { SidebarProvider } from "@/components/ui/sidebar"; 6 | 7 | export const metadata: Metadata = { 8 | title: "Create Next App", 9 | description: "Generated by create next app", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | 20 | 21 | 22 | {children} 23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /services/blog/src/routes/blog.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | addComment, 4 | deleteComment, 5 | getAllBlogs, 6 | getAllComments, 7 | getSavedBlog, 8 | getSingleBlog, 9 | saveBlog, 10 | } from "../controllers/blog.js"; 11 | import { isAuth } from "../middleware/isAuth.js"; 12 | 13 | const router = express.Router(); 14 | 15 | router.get("/blog/all", getAllBlogs); 16 | router.get("/blog/:id", getSingleBlog); 17 | router.post("/comment/:id", isAuth, addComment); 18 | router.get("/comment/:id", getAllComments); 19 | router.delete("/comment/:commentid", isAuth, deleteComment); 20 | router.post("/save/:blogid", isAuth, saveBlog); 21 | router.get("/blog/saved/all", isAuth, getSavedBlog); 22 | 23 | export default router; 24 | -------------------------------------------------------------------------------- /services/author/src/routes/blog.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { isAuth } from "../middlewares/isAuth.js"; 3 | import uploadFile from "../middlewares/multer.js"; 4 | import { 5 | aiBlogResponse, 6 | aiDescriptionResponse, 7 | aiTitleResponse, 8 | createBlog, 9 | deleteBlog, 10 | updateBlog, 11 | } from "../controllers/blog.js"; 12 | 13 | const router = express(); 14 | 15 | router.post("/blog/new", isAuth, uploadFile, createBlog); 16 | router.post("/blog/:id", isAuth, uploadFile, updateBlog); 17 | router.delete("/blog/:id", isAuth, deleteBlog); 18 | router.post("/ai/title", aiTitleResponse); 19 | router.post("/ai/descripiton", aiDescriptionResponse); 20 | router.post("/ai/blog", aiBlogResponse); 21 | 22 | export default router; 23 | -------------------------------------------------------------------------------- /services/user/src/server.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import dotenv from "dotenv"; 3 | import connectDb from "./utils/db.js"; 4 | import userRoutes from "./routes/user.js"; 5 | import { v2 as cloudinary } from "cloudinary"; 6 | import cors from "cors"; 7 | 8 | dotenv.config(); 9 | 10 | cloudinary.config({ 11 | cloud_name: process.env.Cloud_Name, 12 | api_key: process.env.Cloud_Api_Key, 13 | api_secret: process.env.Cloud_Api_Secret, 14 | }); 15 | 16 | const app = express(); 17 | 18 | app.use(express.json()); 19 | app.use(cors()); 20 | 21 | connectDb(); 22 | 23 | app.use("/api/v1", userRoutes); 24 | 25 | const port = process.env.PORT; 26 | 27 | app.listen(port, () => { 28 | console.log(`Server is running on http://localhost:${port}`); 29 | }); 30 | -------------------------------------------------------------------------------- /services/blog/src/server.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import dotenv from "dotenv"; 3 | import blogRoutes from "./routes/blog.js"; 4 | import { createClient } from "redis"; 5 | import { startCacheConsumer } from "./utils/consumer.js"; 6 | import cors from "cors"; 7 | 8 | dotenv.config(); 9 | 10 | const app = express(); 11 | 12 | app.use(express.json()); 13 | app.use(cors()); 14 | 15 | const port = process.env.PORT; 16 | 17 | startCacheConsumer(); 18 | 19 | export const redisClient = createClient({ 20 | url: process.env.REDIS_URL, 21 | }); 22 | 23 | redisClient 24 | .connect() 25 | .then(() => console.log("Connected to redis")) 26 | .catch(console.error); 27 | 28 | app.use("/api/v1", blogRoutes); 29 | 30 | app.listen(port, () => { 31 | console.log(`Server is running on http://localhost:${port}`); 32 | }); 33 | -------------------------------------------------------------------------------- /frontend/src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Separator({ 9 | className, 10 | orientation = "horizontal", 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 25 | ) 26 | } 27 | 28 | export { Separator } 29 | -------------------------------------------------------------------------------- /services/user/src/model/User.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document, Schema } from "mongoose"; 2 | 3 | export interface IUser extends Document { 4 | name: string; 5 | email: string; 6 | image: string; 7 | instagram: string; 8 | facebook: string; 9 | linkedin: string; 10 | bio: string; 11 | } 12 | 13 | const schema: Schema = new Schema( 14 | { 15 | name: { 16 | type: String, 17 | required: true, 18 | }, 19 | email: { 20 | type: String, 21 | required: true, 22 | unique: true, 23 | }, 24 | image: { 25 | type: String, 26 | required: true, 27 | }, 28 | instagram: String, 29 | facebook: String, 30 | linkedin: String, 31 | bio: String, 32 | }, 33 | { 34 | timestamps: true, 35 | } 36 | ); 37 | 38 | const User = mongoose.model("User", schema); 39 | 40 | export default User; 41 | -------------------------------------------------------------------------------- /services/blog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "tsc", 10 | "start": "node dist/server.js", 11 | "dev": "concurrently \"tsc -w\" \"nodemon dist/server.js\"" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@neondatabase/serverless": "^1.0.0", 18 | "@types/amqplib": "^0.10.7", 19 | "@types/axios": "^0.14.4", 20 | "@types/cors": "^2.8.17", 21 | "@types/dotenv": "^8.2.3", 22 | "@types/express": "^5.0.1", 23 | "@types/jsonwebtoken": "^9.0.9", 24 | "amqplib": "^0.10.7", 25 | "axios": "^1.8.4", 26 | "concurrently": "^9.1.2", 27 | "cors": "^2.8.5", 28 | "dotenv": "^16.5.0", 29 | "express": "^5.1.0", 30 | "jsonwebtoken": "^9.0.2", 31 | "redis": "^4.7.0", 32 | "typescript": "^5.8.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ) 19 | } 20 | 21 | export { Input } 22 | -------------------------------------------------------------------------------- /services/user/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "npm install && tsc", 10 | "start": "node dist/server.js", 11 | "dev": "concurrently \"tsc -w\" \"nodemon dist/server.js\"" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@types/axios": "^0.14.4", 18 | "@types/cors": "^2.8.17", 19 | "@types/dotenv": "^8.2.3", 20 | "@types/express": "^5.0.1", 21 | "@types/jsonwebtoken": "^9.0.9", 22 | "@types/mongoose": "^5.11.97", 23 | "@types/multer": "^1.4.12", 24 | "axios": "^1.9.0", 25 | "cloudinary": "^2.6.0", 26 | "concurrently": "^9.1.2", 27 | "cors": "^2.8.5", 28 | "datauri": "^4.1.0", 29 | "dotenv": "^16.5.0", 30 | "express": "^5.1.0", 31 | "googleapis": "^148.0.0", 32 | "jsonwebtoken": "^9.0.2", 33 | "mongoose": "^8.13.2", 34 | "multer": "^1.4.5-lts.2", 35 | "typescript": "^5.8.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /services/author/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "author", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "tsc", 10 | "start": "node dist/server.js", 11 | "dev": "concurrently \"tsc -w\" \"nodemon dist/server.js\"" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@google/genai": "^0.12.0", 18 | "@google/generative-ai": "^0.24.1", 19 | "@neondatabase/serverless": "^1.0.0", 20 | "@types/amqplib": "^0.10.7", 21 | "@types/cors": "^2.8.17", 22 | "@types/dotenv": "^8.2.3", 23 | "@types/express": "^5.0.1", 24 | "@types/jsonwebtoken": "^9.0.9", 25 | "@types/multer": "^1.4.12", 26 | "amqplib": "^0.10.7", 27 | "cloudinary": "^2.6.0", 28 | "concurrently": "^9.1.2", 29 | "cors": "^2.8.5", 30 | "datauri": "^4.1.0", 31 | "dotenv": "^16.5.0", 32 | "express": "^5.1.0", 33 | "jsonwebtoken": "^9.0.2", 34 | "multer": "^1.4.5-lts.2", 35 | "typescript": "^5.8.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /services/user/src/middleware/isAuth.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import jwt, { JwtPayload } from "jsonwebtoken"; 3 | import { IUser } from "../model/User.js"; 4 | 5 | export interface AuthenticatedRequest extends Request { 6 | user?: IUser | null; 7 | } 8 | 9 | export const isAuth = async ( 10 | req: AuthenticatedRequest, 11 | res: Response, 12 | next: NextFunction 13 | ): Promise => { 14 | try { 15 | const authHeader = req.headers.authorization; 16 | 17 | if (!authHeader || !authHeader.startsWith("Bearer ")) { 18 | res.status(401).json({ 19 | message: "Please Login - No auth header", 20 | }); 21 | return; 22 | } 23 | 24 | const token = authHeader.split(" ")[1]; 25 | 26 | const decodeValue = jwt.verify( 27 | token, 28 | process.env.JWT_SEC as string 29 | ) as JwtPayload; 30 | 31 | if (!decodeValue || !decodeValue.user) { 32 | res.status(401).json({ 33 | message: "Invalid token", 34 | }); 35 | return; 36 | } 37 | 38 | req.user = decodeValue.user; 39 | next(); 40 | } catch (error) { 41 | console.log("JWT verification error: ", error); 42 | res.status(401).json({ 43 | message: "Please Login - Jwt error", 44 | }); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /frontend/src/app/blog/saved/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import BlogCard from "@/components/BlogCard"; 3 | import Loading from "@/components/loading"; 4 | import { useAppData } from "@/context/AppContext"; 5 | import React from "react"; 6 | 7 | const SavedBlogs = () => { 8 | const { blogs, savedBlogs } = useAppData(); 9 | 10 | if (!blogs || !savedBlogs) { 11 | return ; 12 | } 13 | 14 | const filteredBlogs = blogs.filter((blog) => 15 | savedBlogs.some((saved) => saved.blogid === blog.id.toString()) 16 | ); 17 | 18 | return ( 19 |
20 |

Saved Blogs

21 |
22 | {filteredBlogs.length > 0 ? ( 23 | filteredBlogs.map((e, i) => { 24 | return ( 25 | 33 | ); 34 | }) 35 | ) : ( 36 |

No saved blogs yet!

37 | )} 38 |
39 |
40 | ); 41 | }; 42 | 43 | export default SavedBlogs; 44 | -------------------------------------------------------------------------------- /frontend/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Avatar({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | function AvatarImage({ 25 | className, 26 | ...props 27 | }: React.ComponentProps) { 28 | return ( 29 | 34 | ) 35 | } 36 | 37 | function AvatarFallback({ 38 | className, 39 | ...props 40 | }: React.ComponentProps) { 41 | return ( 42 | 50 | ) 51 | } 52 | 53 | export { Avatar, AvatarImage, AvatarFallback } 54 | -------------------------------------------------------------------------------- /frontend/src/components/BlogCard.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | import { Card } from "./ui/card"; 4 | import { Calendar } from "lucide-react"; 5 | import moment from "moment"; 6 | 7 | interface BlogCardProps { 8 | image: string; 9 | title: string; 10 | desc: string; 11 | id: string; 12 | time: string; 13 | } 14 | 15 | const BlogCard: React.FC = ({ 16 | image, 17 | title, 18 | desc, 19 | id, 20 | time, 21 | }) => { 22 | return ( 23 | 24 | 25 |
26 | {title} 27 |
28 | 29 |
30 |
31 |

32 | 33 | {moment(time).format("DD-MM-YYYY")} 34 |

35 |

36 | {title} 37 |

38 |

{desc.slice(0, 30)}...

39 |
40 |
41 |
42 | 43 | ); 44 | }; 45 | 46 | export default BlogCard; 47 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-avatar": "^1.1.7", 13 | "@radix-ui/react-dialog": "^1.1.13", 14 | "@radix-ui/react-label": "^2.1.4", 15 | "@radix-ui/react-select": "^2.2.2", 16 | "@radix-ui/react-separator": "^1.1.6", 17 | "@radix-ui/react-slot": "^1.2.2", 18 | "@radix-ui/react-tooltip": "^1.2.6", 19 | "@react-oauth/google": "^0.12.1", 20 | "@types/axios": "^0.14.4", 21 | "@types/js-cookie": "^3.0.6", 22 | "axios": "^1.9.0", 23 | "class-variance-authority": "^0.7.1", 24 | "clsx": "^2.1.1", 25 | "jodit-react": "^5.2.19", 26 | "js-cookie": "^3.0.5", 27 | "lucide-react": "^0.503.0", 28 | "moment": "^2.30.1", 29 | "next": "15.3.1", 30 | "react": "^19.0.0", 31 | "react-dom": "^19.0.0", 32 | "react-hot-toast": "^2.5.2", 33 | "tailwind-merge": "^3.2.0" 34 | }, 35 | "devDependencies": { 36 | "@eslint/eslintrc": "^3", 37 | "@tailwindcss/postcss": "^4", 38 | "@types/node": "^20", 39 | "@types/react": "^19", 40 | "@types/react-dom": "^19", 41 | "eslint": "^9", 42 | "eslint-config-next": "15.3.1", 43 | "tailwindcss": "^4", 44 | "tw-animate-css": "^1.2.8", 45 | "typescript": "^5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /services/author/src/utils/rabbitmq.ts: -------------------------------------------------------------------------------- 1 | import amqp from "amqplib"; 2 | 3 | let channel: amqp.Channel; 4 | 5 | export const connectRabbitMQ = async () => { 6 | try { 7 | const connection = await amqp.connect({ 8 | protocol: "amqp", 9 | hostname: process.env.Rabbimq_Host, 10 | port: 5672, 11 | username: process.env.Rabbimq_Username, 12 | password: process.env.Rabbimq_Password, 13 | }); 14 | 15 | channel = await connection.createChannel(); 16 | 17 | console.log("✅ Connected to Rabbitmq"); 18 | } catch (error) { 19 | console.error("❌ Failed to connect to Rabbitmq", error); 20 | } 21 | }; 22 | 23 | export const publishToQueue = async (queueName: string, message: any) => { 24 | if (!channel) { 25 | console.error("Rabbitmq channel is not intialized"); 26 | return; 27 | } 28 | 29 | await channel.assertQueue(queueName, { durable: true }); 30 | 31 | channel.sendToQueue(queueName, Buffer.from(JSON.stringify(message)), { 32 | persistent: true, 33 | }); 34 | }; 35 | 36 | export const invalidateChacheJob = async (cacheKeys: string[]) => { 37 | try { 38 | const message = { 39 | action: "invalidateCache", 40 | keys: cacheKeys, 41 | }; 42 | 43 | await publishToQueue("cache-invalidation", message); 44 | 45 | console.log("✅ Cache invalidation job published to Rabbitmq"); 46 | } catch (error) { 47 | console.error("❌ Failed to Publish cache on Rabbitmq", error); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /services/author/src/middlewares/isAuth.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import jwt, { JwtPayload } from "jsonwebtoken"; 3 | 4 | interface IUser extends Document { 5 | _id: string; 6 | name: string; 7 | email: string; 8 | image: string; 9 | instagram: string; 10 | facebook: string; 11 | linkedin: string; 12 | bio: string; 13 | } 14 | 15 | export interface AuthenticatedRequest extends Request { 16 | user?: IUser | null; 17 | } 18 | 19 | export const isAuth = async ( 20 | req: AuthenticatedRequest, 21 | res: Response, 22 | next: NextFunction 23 | ): Promise => { 24 | try { 25 | const authHeader = req.headers.authorization; 26 | 27 | if (!authHeader || !authHeader.startsWith("Bearer ")) { 28 | res.status(401).json({ 29 | message: "Please Login - No auth header", 30 | }); 31 | return; 32 | } 33 | 34 | const token = authHeader.split(" ")[1]; 35 | 36 | const decodeValue = jwt.verify( 37 | token, 38 | process.env.JWT_SEC as string 39 | ) as JwtPayload; 40 | 41 | if (!decodeValue || !decodeValue.user) { 42 | res.status(401).json({ 43 | message: "Invalid token", 44 | }); 45 | return; 46 | } 47 | 48 | req.user = decodeValue.user; 49 | next(); 50 | } catch (error) { 51 | console.log("JWT verification error: ", error); 52 | res.status(401).json({ 53 | message: "Please Login - Jwt error", 54 | }); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /services/blog/src/middleware/isAuth.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import jwt, { JwtPayload } from "jsonwebtoken"; 3 | 4 | interface IUser extends Document { 5 | _id: string; 6 | name: string; 7 | email: string; 8 | image: string; 9 | instagram: string; 10 | facebook: string; 11 | linkedin: string; 12 | bio: string; 13 | } 14 | 15 | export interface AuthenticatedRequest extends Request { 16 | user?: IUser | null; 17 | } 18 | 19 | export const isAuth = async ( 20 | req: AuthenticatedRequest, 21 | res: Response, 22 | next: NextFunction 23 | ): Promise => { 24 | try { 25 | const authHeader = req.headers.authorization; 26 | 27 | if (!authHeader || !authHeader.startsWith("Bearer ")) { 28 | res.status(401).json({ 29 | message: "Please Login - No auth header", 30 | }); 31 | return; 32 | } 33 | 34 | const token = authHeader.split(" ")[1]; 35 | 36 | const decodeValue = jwt.verify( 37 | token, 38 | process.env.JWT_SEC as string 39 | ) as JwtPayload; 40 | 41 | if (!decodeValue || !decodeValue.user) { 42 | res.status(401).json({ 43 | message: "Invalid token", 44 | }); 45 | return; 46 | } 47 | 48 | req.user = decodeValue.user; 49 | next(); 50 | } catch (error) { 51 | console.log("JWT verification error: ", error); 52 | res.status(401).json({ 53 | message: "Please Login - Jwt error", 54 | }); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /frontend/src/components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { 4 | Sidebar, 5 | SidebarContent, 6 | SidebarGroup, 7 | SidebarGroupLabel, 8 | SidebarHeader, 9 | SidebarMenu, 10 | SidebarMenuButton, 11 | SidebarMenuItem, 12 | } from "./ui/sidebar"; 13 | import { Input } from "./ui/input"; 14 | import { BoxSelect } from "lucide-react"; 15 | import { blogCategories, useAppData } from "@/context/AppContext"; 16 | 17 | const SideBar = () => { 18 | const { searchQuery, setSearchQuery, setCategory } = useAppData(); 19 | return ( 20 | 21 | 22 | The Reading Retreat 23 | 24 | 25 | 26 | Search 27 | setSearchQuery(e.target.value)} 31 | placeholder="Search Your Desired blog" 32 | /> 33 | 34 | Categories 35 | 36 | 37 | setCategory("")}> 38 | All 39 | 40 | {blogCategories?.map((e, i) => { 41 | return ( 42 | setCategory(e)}> 43 | {e} 44 | 45 | ); 46 | })} 47 | 48 | 49 | 50 | 51 | 52 | ); 53 | }; 54 | 55 | export default SideBar; 56 | -------------------------------------------------------------------------------- /frontend/src/app/blogs/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import BlogCard from "@/components/BlogCard"; 3 | import Loading from "@/components/loading"; 4 | import { Button } from "@/components/ui/button"; 5 | import { useSidebar } from "@/components/ui/sidebar"; 6 | import { useAppData } from "@/context/AppContext"; 7 | import { Filter } from "lucide-react"; 8 | import React from "react"; 9 | 10 | const Blogs = () => { 11 | const { toggleSidebar } = useSidebar(); 12 | const { loading, blogLoading, blogs } = useAppData(); 13 | console.log(blogs); 14 | return ( 15 |
16 | {loading ? ( 17 | 18 | ) : ( 19 |
20 |
21 |

Latest Blogs

22 | 29 |
30 | {blogLoading ? ( 31 | 32 | ) : ( 33 |
34 | {blogs?.length === 0 &&

No Blogs Yet

} 35 | {blogs && 36 | blogs.map((e, i) => { 37 | return ( 38 | 46 | ); 47 | })} 48 |
49 | )} 50 |
51 | )} 52 |
53 | ); 54 | }; 55 | 56 | export default Blogs; 57 | -------------------------------------------------------------------------------- /services/author/src/server.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import dotenv from "dotenv"; 3 | import { sql } from "./utils/db.js"; 4 | import blogRoutes from "./routes/blog.js"; 5 | import { v2 as cloudinary } from "cloudinary"; 6 | import { connectRabbitMQ } from "./utils/rabbitmq.js"; 7 | import cors from "cors"; 8 | 9 | dotenv.config(); 10 | 11 | cloudinary.config({ 12 | cloud_name: process.env.Cloud_Name, 13 | api_key: process.env.Cloud_Api_Key, 14 | api_secret: process.env.Cloud_Api_Secret, 15 | }); 16 | 17 | const app = express(); 18 | 19 | app.use(express.json()); 20 | app.use(cors()); 21 | 22 | connectRabbitMQ(); 23 | 24 | const port = process.env.PORT; 25 | 26 | async function initDB() { 27 | try { 28 | await sql` 29 | CREATE TABLE IF NOT EXISTS blogs( 30 | id SERIAL PRIMARY KEY, 31 | title VARCHAR(255) NOT NULL, 32 | description VARCHAR(255) NOT NULL, 33 | blogcontent TEXT NOT NULL, 34 | image VARCHAR(255) NOT NULL, 35 | category VARCHAR(255) NOT NULL, 36 | author VARCHAR(255) NOT NULL, 37 | create_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 38 | ); 39 | `; 40 | 41 | await sql` 42 | CREATE TABLE IF NOT EXISTS comments( 43 | id SERIAL PRIMARY KEY, 44 | comment VARCHAR(255) NOT NULL, 45 | userid VARCHAR(255) NOT NULL, 46 | username VARCHAR(255) NOT NULL, 47 | blogid VARCHAR(255) NOT NULL, 48 | create_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 49 | ); 50 | `; 51 | 52 | await sql` 53 | CREATE TABLE IF NOT EXISTS savedblogs( 54 | id SERIAL PRIMARY KEY, 55 | userid VARCHAR(255) NOT NULL, 56 | blogid VARCHAR(255) NOT NULL, 57 | create_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 58 | ); 59 | `; 60 | 61 | console.log("database initialized successfully"); 62 | } catch (error) { 63 | console.log("Error initDb", error); 64 | } 65 | } 66 | 67 | app.use("/api/v1", blogRoutes); 68 | 69 | initDB().then(() => { 70 | app.listen(port, () => { 71 | console.log(`Server is running on http://localhost:${port}`); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /frontend/src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function TooltipProvider({ 9 | delayDuration = 0, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 18 | ) 19 | } 20 | 21 | function Tooltip({ 22 | ...props 23 | }: React.ComponentProps) { 24 | return ( 25 | 26 | 27 | 28 | ) 29 | } 30 | 31 | function TooltipTrigger({ 32 | ...props 33 | }: React.ComponentProps) { 34 | return 35 | } 36 | 37 | function TooltipContent({ 38 | className, 39 | sideOffset = 0, 40 | children, 41 | ...props 42 | }: React.ComponentProps) { 43 | return ( 44 | 45 | 54 | {children} 55 | 56 | 57 | 58 | ) 59 | } 60 | 61 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 62 | -------------------------------------------------------------------------------- /frontend/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
15 | ) 16 | } 17 | 18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 | return ( 20 |
28 | ) 29 | } 30 | 31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 32 | return ( 33 |
38 | ) 39 | } 40 | 41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 42 | return ( 43 |
48 | ) 49 | } 50 | 51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) { 52 | return ( 53 |
61 | ) 62 | } 63 | 64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 65 | return ( 66 |
71 | ) 72 | } 73 | 74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 75 | return ( 76 |
81 | ) 82 | } 83 | 84 | export { 85 | Card, 86 | CardHeader, 87 | CardFooter, 88 | CardTitle, 89 | CardAction, 90 | CardDescription, 91 | CardContent, 92 | } 93 | -------------------------------------------------------------------------------- /frontend/src/app/login/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | Card, 6 | CardContent, 7 | CardDescription, 8 | CardHeader, 9 | CardTitle, 10 | } from "@/components/ui/card"; 11 | import axios from "axios"; 12 | import { useAppData, user_service } from "@/context/AppContext"; 13 | import Cookies from "js-cookie"; 14 | import toast from "react-hot-toast"; 15 | import { useGoogleLogin } from "@react-oauth/google"; 16 | import { redirect } from "next/navigation"; 17 | import Loading from "@/components/loading"; 18 | 19 | const LoginPage = () => { 20 | const { isAuth, setIsAuth, loading, setLoading, setUser } = useAppData(); 21 | 22 | if (isAuth) return redirect("/blogs"); 23 | 24 | const responseGoogle = async (authResult: any) => { 25 | setLoading(true); 26 | try { 27 | const result = await axios.post(`${user_service}/api/v1/login`, { 28 | code: authResult["code"], 29 | }); 30 | 31 | Cookies.set("token", result.data.token, { 32 | expires: 5, 33 | secure: true, 34 | path: "/", 35 | }); 36 | toast.success(result.data.message); 37 | setIsAuth(true); 38 | setLoading(false); 39 | setUser(result.data.user); 40 | } catch (error) { 41 | console.log("error", error); 42 | toast.error("Problem while login you"); 43 | setLoading(false); 44 | } 45 | }; 46 | 47 | const googleLogin = useGoogleLogin({ 48 | onSuccess: responseGoogle, 49 | onError: responseGoogle, 50 | flow: "auth-code", 51 | }); 52 | return ( 53 | <> 54 | {loading ? ( 55 | 56 | ) : ( 57 |
58 | 59 | 60 | Login to The Reading Retreat 61 | Your go to blog app 62 | 63 | 64 | 72 | 73 | 74 |
75 | )} 76 | 77 | ); 78 | }; 79 | 80 | export default LoginPage; 81 | -------------------------------------------------------------------------------- /frontend/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90 cursor-pointer", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 cursor-pointer", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 cursor-pointer", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80 cursor-pointer", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 cursor-pointer", 22 | link: "text-primary underline-offset-4 hover:underline cursor-pointer", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ); 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean; 47 | }) { 48 | const Comp = asChild ? Slot : "button"; 49 | 50 | return ( 51 | 56 | ); 57 | } 58 | 59 | export { Button, buttonVariants }; 60 | -------------------------------------------------------------------------------- /services/blog/src/utils/consumer.ts: -------------------------------------------------------------------------------- 1 | import amqp from "amqplib"; 2 | import { redisClient } from "../server.js"; 3 | import { sql } from "./db.js"; 4 | 5 | interface CacheInvalidationMessage { 6 | action: string; 7 | keys: string[]; 8 | } 9 | 10 | export const startCacheConsumer = async () => { 11 | try { 12 | const connection = await amqp.connect({ 13 | protocol: "amqp", 14 | hostname: process.env.Rabbimq_Host, 15 | port: 5672, 16 | username: process.env.Rabbimq_Username, 17 | password: process.env.Rabbimq_Password, 18 | }); 19 | 20 | const channel = await connection.createChannel(); 21 | 22 | const queueName = "cache-invalidation"; 23 | 24 | await channel.assertQueue(queueName, { durable: true }); 25 | 26 | console.log("✅ Blog Service cache consumer started"); 27 | 28 | channel.consume(queueName, async (msg) => { 29 | if (msg) { 30 | try { 31 | const content = JSON.parse( 32 | msg.content.toString() 33 | ) as CacheInvalidationMessage; 34 | 35 | console.log( 36 | "📩 Blog service recieved cache invalidation message", 37 | content 38 | ); 39 | 40 | if (content.action === "invalidateCache") { 41 | for (const pattern of content.keys) { 42 | const keys = await redisClient.keys(pattern); 43 | 44 | if (keys.length > 0) { 45 | await redisClient.del(keys); 46 | 47 | console.log( 48 | `🗑️ Blog service invalidated ${keys.length} cache keys matching: ${pattern}` 49 | ); 50 | 51 | const category = ""; 52 | 53 | const searchQuery = ""; 54 | 55 | const cacheKey = `blogs:${searchQuery}:${category}`; 56 | 57 | const blogs = 58 | await sql`SELECT * FROM blogs ORDER BY create_at DESC`; 59 | 60 | await redisClient.set(cacheKey, JSON.stringify(blogs), { 61 | EX: 3600, 62 | }); 63 | 64 | console.log("🔄️ Cache rebuilt with key:", cacheKey); 65 | } 66 | } 67 | } 68 | 69 | channel.ack(msg); 70 | } catch (error) { 71 | console.error( 72 | "❌ Error processing cache invalidation in blog service:", 73 | error 74 | ); 75 | 76 | channel.nack(msg, false, true); 77 | } 78 | } 79 | }); 80 | } catch (error) { 81 | console.error("❌ Failed to start rabbitmq consumer"); 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /frontend/src/components/navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from "next/link"; 3 | import React, { useState } from "react"; 4 | import { Button } from "./ui/button"; 5 | import { CircleUserRoundIcon, LogIn, Menu, X } from "lucide-react"; 6 | import { cn } from "@/lib/utils"; 7 | import { useAppData } from "@/context/AppContext"; 8 | 9 | const Navbar = () => { 10 | const [isOpen, setIsOpen] = useState(false); 11 | 12 | const { loading, isAuth } = useAppData(); 13 | 14 | return ( 15 | 93 | ); 94 | }; 95 | 96 | export default Navbar; 97 | -------------------------------------------------------------------------------- /services/user/src/controllers/user.ts: -------------------------------------------------------------------------------- 1 | import User from "../model/User.js"; 2 | import jwt from "jsonwebtoken"; 3 | import TryCatch from "../utils/TryCatch.js"; 4 | import { AuthenticatedRequest } from "../middleware/isAuth.js"; 5 | import getBuffer from "../utils/dataUri.js"; 6 | import { v2 as cloudinary } from "cloudinary"; 7 | import { oauth2client } from "../utils/GoogleConfig.js"; 8 | import axios from "axios"; 9 | 10 | export const loginUser = TryCatch(async (req, res) => { 11 | const { code } = req.body; 12 | 13 | if (!code) { 14 | res.status(400).json({ 15 | message: "Authorization code is required", 16 | }); 17 | return; 18 | } 19 | 20 | const googleRes = await oauth2client.getToken(code); 21 | 22 | oauth2client.setCredentials(googleRes.tokens); 23 | 24 | const userRes = await axios.get( 25 | `https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=${googleRes.tokens.access_token}` 26 | ); 27 | 28 | const { email, name, picture } = userRes.data; 29 | 30 | let user = await User.findOne({ email }); 31 | 32 | if (!user) { 33 | user = await User.create({ 34 | name, 35 | email, 36 | image: picture, 37 | }); 38 | } 39 | 40 | const token = jwt.sign({ user }, process.env.JWT_SEC as string, { 41 | expiresIn: "5d", 42 | }); 43 | 44 | res.status(200).json({ 45 | message: "Login success", 46 | token, 47 | user, 48 | }); 49 | }); 50 | 51 | export const myProfile = TryCatch(async (req: AuthenticatedRequest, res) => { 52 | const user = req.user; 53 | 54 | res.json(user); 55 | }); 56 | 57 | export const getUserProfile = TryCatch(async (req, res) => { 58 | const user = await User.findById(req.params.id); 59 | 60 | if (!user) { 61 | res.status(404).json({ 62 | message: "No user with this id", 63 | }); 64 | return; 65 | } 66 | 67 | res.json(user); 68 | }); 69 | 70 | export const updateUser = TryCatch(async (req: AuthenticatedRequest, res) => { 71 | const { name, instagram, facebook, linkedin, bio } = req.body; 72 | 73 | const user = await User.findByIdAndUpdate( 74 | req.user?._id, 75 | { 76 | name, 77 | instagram, 78 | facebook, 79 | linkedin, 80 | bio, 81 | }, 82 | { new: true } 83 | ); 84 | 85 | const token = jwt.sign({ user }, process.env.JWT_SEC as string, { 86 | expiresIn: "5d", 87 | }); 88 | 89 | res.json({ 90 | message: "User Updated", 91 | token, 92 | user, 93 | }); 94 | }); 95 | 96 | export const updateProfilePic = TryCatch( 97 | async (req: AuthenticatedRequest, res) => { 98 | const file = req.file; 99 | 100 | if (!file) { 101 | res.status(400).json({ 102 | message: "No file to upload", 103 | }); 104 | return; 105 | } 106 | 107 | const fileBuffer = getBuffer(file); 108 | 109 | if (!fileBuffer || !fileBuffer.content) { 110 | res.status(400).json({ 111 | message: "Failed to generate buffer", 112 | }); 113 | return; 114 | } 115 | const cloud = await cloudinary.uploader.upload(fileBuffer.content, { 116 | folder: "blogs", 117 | }); 118 | 119 | const user = await User.findByIdAndUpdate( 120 | req.user?._id, 121 | { 122 | image: cloud.secure_url, 123 | }, 124 | { new: true } 125 | ); 126 | 127 | const token = jwt.sign({ user }, process.env.JWT_SEC as string, { 128 | expiresIn: "5d", 129 | }); 130 | 131 | res.json({ 132 | message: "User Profile pic updated", 133 | token, 134 | user, 135 | }); 136 | } 137 | ); 138 | -------------------------------------------------------------------------------- /frontend/src/app/profile/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Avatar, AvatarImage } from "@/components/ui/avatar"; 3 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 4 | import { useAppData, User, user_service } from "@/context/AppContext"; 5 | import React, { useEffect, useRef, useState } from "react"; 6 | import Cookies from "js-cookie"; 7 | import axios from "axios"; 8 | import toast from "react-hot-toast"; 9 | import Loading from "@/components/loading"; 10 | import { Facebook, Instagram, Linkedin } from "lucide-react"; 11 | import { Button } from "@/components/ui/button"; 12 | import { 13 | Dialog, 14 | DialogContent, 15 | DialogHeader, 16 | DialogTitle, 17 | DialogTrigger, 18 | } from "@/components/ui/dialog"; 19 | import { Label } from "@/components/ui/label"; 20 | import { Input } from "@/components/ui/input"; 21 | import { redirect, useParams, useRouter } from "next/navigation"; 22 | 23 | const UserProfilePage = () => { 24 | const [user, setUser] = useState(null); 25 | 26 | const { id } = useParams(); 27 | 28 | async function fetchUser() { 29 | try { 30 | const { data } = await axios.get(`${user_service}/api/v1/user/${id}`); 31 | setUser(data); 32 | } catch (error) { 33 | console.log(error); 34 | } 35 | } 36 | 37 | useEffect(() => { 38 | fetchUser(); 39 | }, [id]); 40 | 41 | if (!user) { 42 | return ; 43 | } 44 | return ( 45 |
46 | 47 | 48 | Profile 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 | 57 |

{user?.name}

58 |
59 | 60 | {user?.bio && ( 61 |
62 | 63 |

{user.bio}

64 |
65 | )} 66 | 67 |
68 | {user?.instagram && ( 69 | 74 | 75 | 76 | )} 77 | 78 | {user?.facebook && ( 79 | 84 | 85 | 86 | )} 87 | 88 | {user?.linkedin && ( 89 | 94 | 95 | 96 | )} 97 |
98 |
99 |
100 |
101 |
102 | ); 103 | }; 104 | 105 | export default UserProfilePage; 106 | -------------------------------------------------------------------------------- /frontend/src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { XIcon } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | function Dialog({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return 13 | } 14 | 15 | function DialogTrigger({ 16 | ...props 17 | }: React.ComponentProps) { 18 | return 19 | } 20 | 21 | function DialogPortal({ 22 | ...props 23 | }: React.ComponentProps) { 24 | return 25 | } 26 | 27 | function DialogClose({ 28 | ...props 29 | }: React.ComponentProps) { 30 | return 31 | } 32 | 33 | function DialogOverlay({ 34 | className, 35 | ...props 36 | }: React.ComponentProps) { 37 | return ( 38 | 46 | ) 47 | } 48 | 49 | function DialogContent({ 50 | className, 51 | children, 52 | ...props 53 | }: React.ComponentProps) { 54 | return ( 55 | 56 | 57 | 65 | {children} 66 | 67 | 68 | Close 69 | 70 | 71 | 72 | ) 73 | } 74 | 75 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { 76 | return ( 77 |
82 | ) 83 | } 84 | 85 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { 86 | return ( 87 |
95 | ) 96 | } 97 | 98 | function DialogTitle({ 99 | className, 100 | ...props 101 | }: React.ComponentProps) { 102 | return ( 103 | 108 | ) 109 | } 110 | 111 | function DialogDescription({ 112 | className, 113 | ...props 114 | }: React.ComponentProps) { 115 | return ( 116 | 121 | ) 122 | } 123 | 124 | export { 125 | Dialog, 126 | DialogClose, 127 | DialogContent, 128 | DialogDescription, 129 | DialogFooter, 130 | DialogHeader, 131 | DialogOverlay, 132 | DialogPortal, 133 | DialogTitle, 134 | DialogTrigger, 135 | } 136 | -------------------------------------------------------------------------------- /frontend/src/components/ui/sheet.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SheetPrimitive from "@radix-ui/react-dialog" 5 | import { XIcon } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | function Sheet({ ...props }: React.ComponentProps) { 10 | return 11 | } 12 | 13 | function SheetTrigger({ 14 | ...props 15 | }: React.ComponentProps) { 16 | return 17 | } 18 | 19 | function SheetClose({ 20 | ...props 21 | }: React.ComponentProps) { 22 | return 23 | } 24 | 25 | function SheetPortal({ 26 | ...props 27 | }: React.ComponentProps) { 28 | return 29 | } 30 | 31 | function SheetOverlay({ 32 | className, 33 | ...props 34 | }: React.ComponentProps) { 35 | return ( 36 | 44 | ) 45 | } 46 | 47 | function SheetContent({ 48 | className, 49 | children, 50 | side = "right", 51 | ...props 52 | }: React.ComponentProps & { 53 | side?: "top" | "right" | "bottom" | "left" 54 | }) { 55 | return ( 56 | 57 | 58 | 74 | {children} 75 | 76 | 77 | Close 78 | 79 | 80 | 81 | ) 82 | } 83 | 84 | function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { 85 | return ( 86 |
91 | ) 92 | } 93 | 94 | function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { 95 | return ( 96 |
101 | ) 102 | } 103 | 104 | function SheetTitle({ 105 | className, 106 | ...props 107 | }: React.ComponentProps) { 108 | return ( 109 | 114 | ) 115 | } 116 | 117 | function SheetDescription({ 118 | className, 119 | ...props 120 | }: React.ComponentProps) { 121 | return ( 122 | 127 | ) 128 | } 129 | 130 | export { 131 | Sheet, 132 | SheetTrigger, 133 | SheetClose, 134 | SheetContent, 135 | SheetHeader, 136 | SheetFooter, 137 | SheetTitle, 138 | SheetDescription, 139 | } 140 | -------------------------------------------------------------------------------- /services/blog/src/controllers/blog.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticatedRequest } from "../middleware/isAuth.js"; 2 | import { redisClient } from "../server.js"; 3 | import { sql } from "../utils/db.js"; 4 | import TryCatch from "../utils/TryCatch.js"; 5 | import axios from "axios"; 6 | 7 | export const getAllBlogs = TryCatch(async (req, res) => { 8 | const { searchQuery = "", category = "" } = req.query; 9 | 10 | const cacheKey = `blogs:${searchQuery}:${category}`; 11 | 12 | const cached = await redisClient.get(cacheKey); 13 | 14 | if (cached) { 15 | console.log("Serving from Redis cache"); 16 | res.json(JSON.parse(cached)); 17 | return; 18 | } 19 | let blogs; 20 | 21 | if (searchQuery && category) { 22 | blogs = await sql`SELECT * FROM blogs WHERE (title ILIKE ${ 23 | "%" + searchQuery + "%" 24 | } OR description ILIKE ${ 25 | "%" + searchQuery + "%" 26 | }) AND category = ${category} ORDER BY create_at DESC`; 27 | } else if (searchQuery) { 28 | blogs = await sql`SELECT * FROM blogs WHERE (title ILIKE ${ 29 | "%" + searchQuery + "%" 30 | } OR description ILIKE ${"%" + searchQuery + "%"}) ORDER BY create_at DESC`; 31 | } else if (category) { 32 | blogs = 33 | await sql`SELECT * FROM blogs WHERE category=${category} ORDER BY create_at DESC`; 34 | } else { 35 | blogs = await sql`SELECT * FROM blogs ORDER BY create_at DESC`; 36 | } 37 | 38 | console.log("Serving from db"); 39 | 40 | await redisClient.set(cacheKey, JSON.stringify(blogs), { EX: 3600 }); 41 | 42 | res.json(blogs); 43 | }); 44 | 45 | export const getSingleBlog = TryCatch(async (req, res) => { 46 | const blogid = req.params.id; 47 | 48 | const cacheKey = `blog:${blogid}`; 49 | 50 | const cached = await redisClient.get(cacheKey); 51 | 52 | if (cached) { 53 | console.log("Serving single blog from Redis cache"); 54 | res.json(JSON.parse(cached)); 55 | return; 56 | } 57 | 58 | const blog = await sql`SELECT * FROM blogs WHERE id = ${blogid}`; 59 | 60 | if (blog.length === 0) { 61 | res.status(404).json({ 62 | message: "no blog with this id", 63 | }); 64 | return; 65 | } 66 | 67 | const { data } = await axios.get( 68 | `${process.env.USER_SERVICE}/api/v1/user/${blog[0].author}` 69 | ); 70 | 71 | const responseData = { blog: blog[0], author: data }; 72 | 73 | await redisClient.set(cacheKey, JSON.stringify(responseData), { EX: 3600 }); 74 | 75 | res.json(responseData); 76 | }); 77 | 78 | export const addComment = TryCatch(async (req: AuthenticatedRequest, res) => { 79 | const { id: blogid } = req.params; 80 | const { comment } = req.body; 81 | 82 | await sql`INSERT INTO comments (comment, blogid, userid, username) VALUES (${comment}, ${blogid}, ${req.user?._id}, ${req.user?.name}) RETURNING *`; 83 | 84 | res.json({ 85 | message: "Comment Added", 86 | }); 87 | }); 88 | 89 | export const getAllComments = TryCatch(async (req, res) => { 90 | const { id } = req.params; 91 | 92 | const comments = 93 | await sql`SELECT * FROM comments WHERE blogid = ${id} ORDER BY create_at DESC`; 94 | 95 | res.json(comments); 96 | }); 97 | 98 | export const deleteComment = TryCatch( 99 | async (req: AuthenticatedRequest, res) => { 100 | const { commentid } = req.params; 101 | 102 | const comment = await sql`SELECT * FROM comments WHERE id = ${commentid}`; 103 | 104 | console.log(comment); 105 | 106 | if (comment[0].userid !== req.user?._id) { 107 | res.status(401).json({ 108 | message: "You are not owner of this comment", 109 | }); 110 | return; 111 | } 112 | 113 | await sql`DELETE FROM comments WHERE id = ${commentid}`; 114 | 115 | res.json({ 116 | message: "Comment Deleted", 117 | }); 118 | } 119 | ); 120 | 121 | export const saveBlog = TryCatch(async (req: AuthenticatedRequest, res) => { 122 | const { blogid } = req.params; 123 | const userid = req.user?._id; 124 | 125 | if (!blogid || !userid) { 126 | res.status(400).json({ 127 | message: "Missing blog id or userid", 128 | }); 129 | return; 130 | } 131 | 132 | const existing = 133 | await sql`SELECT * FROM savedblogs WHERE userid = ${userid} AND blogid = ${blogid}`; 134 | 135 | if (existing.length === 0) { 136 | await sql`INSERT INTO savedblogs (blogid, userid) VALUES (${blogid}, ${userid})`; 137 | 138 | res.json({ 139 | message: "Blog Saved", 140 | }); 141 | return; 142 | } else { 143 | await sql`DELETE FROM savedblogs WHERE userid = ${userid} AND blogid = ${blogid}`; 144 | 145 | res.json({ 146 | message: "Blog Unsaved", 147 | }); 148 | return; 149 | } 150 | }); 151 | 152 | export const getSavedBlog = TryCatch(async (req: AuthenticatedRequest, res) => { 153 | const blogs = 154 | await sql`SELECT * FROM savedblogs WHERE userid = ${req.user?._id}`; 155 | 156 | res.json(blogs); 157 | }); 158 | -------------------------------------------------------------------------------- /frontend/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "tw-animate-css"; 3 | 4 | @custom-variant dark (&:is(.dark *)); 5 | 6 | @theme inline { 7 | --color-background: var(--background); 8 | --color-foreground: var(--foreground); 9 | --font-sans: var(--font-geist-sans); 10 | --font-mono: var(--font-geist-mono); 11 | --color-sidebar-ring: var(--sidebar-ring); 12 | --color-sidebar-border: var(--sidebar-border); 13 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 14 | --color-sidebar-accent: var(--sidebar-accent); 15 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 16 | --color-sidebar-primary: var(--sidebar-primary); 17 | --color-sidebar-foreground: var(--sidebar-foreground); 18 | --color-sidebar: var(--sidebar); 19 | --color-chart-5: var(--chart-5); 20 | --color-chart-4: var(--chart-4); 21 | --color-chart-3: var(--chart-3); 22 | --color-chart-2: var(--chart-2); 23 | --color-chart-1: var(--chart-1); 24 | --color-ring: var(--ring); 25 | --color-input: var(--input); 26 | --color-border: var(--border); 27 | --color-destructive: var(--destructive); 28 | --color-accent-foreground: var(--accent-foreground); 29 | --color-accent: var(--accent); 30 | --color-muted-foreground: var(--muted-foreground); 31 | --color-muted: var(--muted); 32 | --color-secondary-foreground: var(--secondary-foreground); 33 | --color-secondary: var(--secondary); 34 | --color-primary-foreground: var(--primary-foreground); 35 | --color-primary: var(--primary); 36 | --color-popover-foreground: var(--popover-foreground); 37 | --color-popover: var(--popover); 38 | --color-card-foreground: var(--card-foreground); 39 | --color-card: var(--card); 40 | --radius-sm: calc(var(--radius) - 4px); 41 | --radius-md: calc(var(--radius) - 2px); 42 | --radius-lg: var(--radius); 43 | --radius-xl: calc(var(--radius) + 4px); 44 | } 45 | 46 | :root { 47 | --radius: 0.625rem; 48 | --background: oklch(1 0 0); 49 | --foreground: oklch(0.129 0.042 264.695); 50 | --card: oklch(1 0 0); 51 | --card-foreground: oklch(0.129 0.042 264.695); 52 | --popover: oklch(1 0 0); 53 | --popover-foreground: oklch(0.129 0.042 264.695); 54 | --primary: oklch(0.208 0.042 265.755); 55 | --primary-foreground: oklch(0.984 0.003 247.858); 56 | --secondary: oklch(0.968 0.007 247.896); 57 | --secondary-foreground: oklch(0.208 0.042 265.755); 58 | --muted: oklch(0.968 0.007 247.896); 59 | --muted-foreground: oklch(0.554 0.046 257.417); 60 | --accent: oklch(0.968 0.007 247.896); 61 | --accent-foreground: oklch(0.208 0.042 265.755); 62 | --destructive: oklch(0.577 0.245 27.325); 63 | --border: oklch(0.929 0.013 255.508); 64 | --input: oklch(0.929 0.013 255.508); 65 | --ring: oklch(0.704 0.04 256.788); 66 | --chart-1: oklch(0.646 0.222 41.116); 67 | --chart-2: oklch(0.6 0.118 184.704); 68 | --chart-3: oklch(0.398 0.07 227.392); 69 | --chart-4: oklch(0.828 0.189 84.429); 70 | --chart-5: oklch(0.769 0.188 70.08); 71 | --sidebar: oklch(0.984 0.003 247.858); 72 | --sidebar-foreground: oklch(0.129 0.042 264.695); 73 | --sidebar-primary: oklch(0.208 0.042 265.755); 74 | --sidebar-primary-foreground: oklch(0.984 0.003 247.858); 75 | --sidebar-accent: oklch(0.968 0.007 247.896); 76 | --sidebar-accent-foreground: oklch(0.208 0.042 265.755); 77 | --sidebar-border: oklch(0.929 0.013 255.508); 78 | --sidebar-ring: oklch(0.704 0.04 256.788); 79 | } 80 | 81 | .dark { 82 | --background: oklch(0.129 0.042 264.695); 83 | --foreground: oklch(0.984 0.003 247.858); 84 | --card: oklch(0.208 0.042 265.755); 85 | --card-foreground: oklch(0.984 0.003 247.858); 86 | --popover: oklch(0.208 0.042 265.755); 87 | --popover-foreground: oklch(0.984 0.003 247.858); 88 | --primary: oklch(0.929 0.013 255.508); 89 | --primary-foreground: oklch(0.208 0.042 265.755); 90 | --secondary: oklch(0.279 0.041 260.031); 91 | --secondary-foreground: oklch(0.984 0.003 247.858); 92 | --muted: oklch(0.279 0.041 260.031); 93 | --muted-foreground: oklch(0.704 0.04 256.788); 94 | --accent: oklch(0.279 0.041 260.031); 95 | --accent-foreground: oklch(0.984 0.003 247.858); 96 | --destructive: oklch(0.704 0.191 22.216); 97 | --border: oklch(1 0 0 / 10%); 98 | --input: oklch(1 0 0 / 15%); 99 | --ring: oklch(0.551 0.027 264.364); 100 | --chart-1: oklch(0.488 0.243 264.376); 101 | --chart-2: oklch(0.696 0.17 162.48); 102 | --chart-3: oklch(0.769 0.188 70.08); 103 | --chart-4: oklch(0.627 0.265 303.9); 104 | --chart-5: oklch(0.645 0.246 16.439); 105 | --sidebar: oklch(0.208 0.042 265.755); 106 | --sidebar-foreground: oklch(0.984 0.003 247.858); 107 | --sidebar-primary: oklch(0.488 0.243 264.376); 108 | --sidebar-primary-foreground: oklch(0.984 0.003 247.858); 109 | --sidebar-accent: oklch(0.279 0.041 260.031); 110 | --sidebar-accent-foreground: oklch(0.984 0.003 247.858); 111 | --sidebar-border: oklch(1 0 0 / 10%); 112 | --sidebar-ring: oklch(0.551 0.027 264.364); 113 | } 114 | 115 | @layer base { 116 | * { 117 | @apply border-border outline-ring/50; 118 | } 119 | body { 120 | @apply bg-background text-foreground; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /frontend/src/context/AppContext.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { 4 | createContext, 5 | ReactNode, 6 | useContext, 7 | useEffect, 8 | useState, 9 | } from "react"; 10 | import Cookies from "js-cookie"; 11 | import axios from "axios"; 12 | import toast, { Toaster } from "react-hot-toast"; 13 | import { GoogleOAuthProvider } from "@react-oauth/google"; 14 | import { get } from "http"; 15 | 16 | export const user_service = "http://localhost:5000"; 17 | export const author_service = "http://localhost:5001"; 18 | export const blog_service = "http://localhost:5002"; 19 | 20 | export const blogCategories = [ 21 | "Techonlogy", 22 | "Health", 23 | "Finance", 24 | "Travel", 25 | "Education", 26 | "Entertainment", 27 | "Study", 28 | ]; 29 | 30 | export interface User { 31 | _id: string; 32 | name: string; 33 | email: string; 34 | image: string; 35 | instagram: string; 36 | facebook: string; 37 | linkedin: string; 38 | bio: string; 39 | } 40 | 41 | export interface Blog { 42 | id: string; 43 | title: string; 44 | description: string; 45 | blogcontent: string; 46 | image: string; 47 | category: string; 48 | author: string; 49 | created_at: string; 50 | } 51 | 52 | interface SavedBlogType { 53 | id: string; 54 | userid: string; 55 | blogid: string; 56 | create_at: string; 57 | } 58 | 59 | interface AppContextType { 60 | user: User | null; 61 | loading: boolean; 62 | isAuth: boolean; 63 | setUser: React.Dispatch>; 64 | setLoading: React.Dispatch>; 65 | setIsAuth: React.Dispatch>; 66 | logoutUser: () => Promise; 67 | blogs: Blog[] | null; 68 | blogLoading: boolean; 69 | setSearchQuery: React.Dispatch>; 70 | searchQuery: string; 71 | setCategory: React.Dispatch>; 72 | fetchBlogs: () => Promise; 73 | savedBlogs: SavedBlogType[] | null; 74 | getSavedBlogs: () => Promise; 75 | } 76 | 77 | const AppContext = createContext(undefined); 78 | 79 | interface AppProviderProps { 80 | children: ReactNode; 81 | } 82 | 83 | export const AppProvider: React.FC = ({ children }) => { 84 | const [user, setUser] = useState(null); 85 | const [isAuth, setIsAuth] = useState(false); 86 | const [loading, setLoading] = useState(true); 87 | 88 | async function fetchUser() { 89 | try { 90 | const token = Cookies.get("token"); 91 | 92 | const { data } = await axios.get(`${user_service}/api/v1/me`, { 93 | headers: { 94 | Authorization: `Bearer ${token}`, 95 | }, 96 | }); 97 | 98 | setUser(data); 99 | setIsAuth(true); 100 | setLoading(false); 101 | } catch (error) { 102 | console.log(error); 103 | setLoading(false); 104 | } 105 | } 106 | 107 | const [blogLoading, setBlogLoading] = useState(true); 108 | 109 | const [blogs, setBlogs] = useState(null); 110 | const [category, setCategory] = useState(""); 111 | const [searchQuery, setSearchQuery] = useState(""); 112 | 113 | async function fetchBlogs() { 114 | setBlogLoading(true); 115 | try { 116 | const { data } = await axios.get( 117 | `${blog_service}/api/v1/blog/all?searchQuery=${searchQuery}&category=${category}` 118 | ); 119 | 120 | setBlogs(data); 121 | } catch (error) { 122 | console.log(error); 123 | } finally { 124 | setBlogLoading(false); 125 | } 126 | } 127 | 128 | const [savedBlogs, setSavedBlogs] = useState(null); 129 | 130 | async function getSavedBlogs() { 131 | const token = Cookies.get("token"); 132 | try { 133 | const { data } = await axios.get( 134 | `${blog_service}/api/v1/blog/saved/all`, 135 | { 136 | headers: { 137 | Authorization: `Bearer ${token}`, 138 | }, 139 | } 140 | ); 141 | setSavedBlogs(data); 142 | } catch (error) { 143 | console.log(error); 144 | } 145 | } 146 | 147 | async function logoutUser() { 148 | Cookies.remove("token"); 149 | setUser(null); 150 | setIsAuth(false); 151 | 152 | toast.success("user Logged Out"); 153 | } 154 | 155 | useEffect(() => { 156 | fetchUser(); 157 | getSavedBlogs(); 158 | }, []); 159 | 160 | useEffect(() => { 161 | fetchBlogs(); 162 | }, [searchQuery, category]); 163 | return ( 164 | 183 | 184 | {children} 185 | 186 | 187 | 188 | ); 189 | }; 190 | 191 | export const useAppData = (): AppContextType => { 192 | const context = useContext(AppContext); 193 | if (!context) { 194 | throw new Error("useappdata must be used within AppProvider"); 195 | } 196 | return context; 197 | }; 198 | -------------------------------------------------------------------------------- /frontend/src/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SelectPrimitive from "@radix-ui/react-select" 5 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | function Select({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return 13 | } 14 | 15 | function SelectGroup({ 16 | ...props 17 | }: React.ComponentProps) { 18 | return 19 | } 20 | 21 | function SelectValue({ 22 | ...props 23 | }: React.ComponentProps) { 24 | return 25 | } 26 | 27 | function SelectTrigger({ 28 | className, 29 | size = "default", 30 | children, 31 | ...props 32 | }: React.ComponentProps & { 33 | size?: "sm" | "default" 34 | }) { 35 | return ( 36 | 45 | {children} 46 | 47 | 48 | 49 | 50 | ) 51 | } 52 | 53 | function SelectContent({ 54 | className, 55 | children, 56 | position = "popper", 57 | ...props 58 | }: React.ComponentProps) { 59 | return ( 60 | 61 | 72 | 73 | 80 | {children} 81 | 82 | 83 | 84 | 85 | ) 86 | } 87 | 88 | function SelectLabel({ 89 | className, 90 | ...props 91 | }: React.ComponentProps) { 92 | return ( 93 | 98 | ) 99 | } 100 | 101 | function SelectItem({ 102 | className, 103 | children, 104 | ...props 105 | }: React.ComponentProps) { 106 | return ( 107 | 115 | 116 | 117 | 118 | 119 | 120 | {children} 121 | 122 | ) 123 | } 124 | 125 | function SelectSeparator({ 126 | className, 127 | ...props 128 | }: React.ComponentProps) { 129 | return ( 130 | 135 | ) 136 | } 137 | 138 | function SelectScrollUpButton({ 139 | className, 140 | ...props 141 | }: React.ComponentProps) { 142 | return ( 143 | 151 | 152 | 153 | ) 154 | } 155 | 156 | function SelectScrollDownButton({ 157 | className, 158 | ...props 159 | }: React.ComponentProps) { 160 | return ( 161 | 169 | 170 | 171 | ) 172 | } 173 | 174 | export { 175 | Select, 176 | SelectContent, 177 | SelectGroup, 178 | SelectItem, 179 | SelectLabel, 180 | SelectScrollDownButton, 181 | SelectScrollUpButton, 182 | SelectSeparator, 183 | SelectTrigger, 184 | SelectValue, 185 | } 186 | -------------------------------------------------------------------------------- /frontend/src/app/blog/edit/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Button } from "@/components/ui/button"; 3 | import { Card, CardContent, CardHeader } from "@/components/ui/card"; 4 | import { Input } from "@/components/ui/input"; 5 | import { Label } from "@/components/ui/label"; 6 | import { 7 | Select, 8 | SelectContent, 9 | SelectItem, 10 | SelectTrigger, 11 | SelectValue, 12 | } from "@/components/ui/select"; 13 | import React, { useEffect, useMemo, useRef, useState } from "react"; 14 | import dynamic from "next/dynamic"; 15 | import Cookies from "js-cookie"; 16 | import axios from "axios"; 17 | import { 18 | author_service, 19 | blog_service, 20 | blogCategories, 21 | useAppData, 22 | } from "@/context/AppContext"; 23 | import toast from "react-hot-toast"; 24 | import { useParams, useRouter } from "next/navigation"; 25 | 26 | const JoditEditor = dynamic(() => import("jodit-react"), { ssr: false }); 27 | 28 | const EditBlogPage = () => { 29 | const editor = useRef(null); 30 | const [content, setContent] = useState(""); 31 | const router = useRouter(); 32 | 33 | const { fetchBlogs } = useAppData(); 34 | 35 | const { id } = useParams(); 36 | 37 | const [loading, setLoading] = useState(false); 38 | const [formData, setFormData] = useState({ 39 | title: "", 40 | description: "", 41 | category: "", 42 | image: null, 43 | blogcontent: "", 44 | }); 45 | 46 | const handleInputChange = (e: any) => { 47 | setFormData({ ...formData, [e.target.name]: e.target.value }); 48 | }; 49 | 50 | const handleFileChange = (e: any) => { 51 | const file = e.target.files[0]; 52 | setFormData({ ...formData, image: file }); 53 | }; 54 | 55 | const config = useMemo( 56 | () => ({ 57 | readonly: false, // all options from https://xdsoft.net/jodit/docs/, 58 | placeholder: "Start typings...", 59 | }), 60 | [] 61 | ); 62 | 63 | const [existingImage, setExistingImage] = useState(null); 64 | 65 | useEffect(() => { 66 | const fetchBlog = async () => { 67 | setLoading(true); 68 | try { 69 | const { data } = await axios.get(`${blog_service}/api/v1/blog/${id}`); 70 | const blog = data.blog; 71 | 72 | setFormData({ 73 | title: blog.title, 74 | description: blog.description, 75 | category: blog.category, 76 | image: null, 77 | blogcontent: blog.blogcontent, 78 | }); 79 | 80 | setContent(blog.blogcontent); 81 | setExistingImage(blog.image); 82 | } catch (error) { 83 | console.log(error); 84 | } finally { 85 | setLoading(false); 86 | } 87 | }; 88 | if (id) fetchBlog(); 89 | }, [id]); 90 | 91 | const handleSubmit = async (e: any) => { 92 | e.preventDefault(); 93 | setLoading(true); 94 | 95 | const fromDataToSend = new FormData(); 96 | 97 | fromDataToSend.append("title", formData.title); 98 | fromDataToSend.append("description", formData.description); 99 | fromDataToSend.append("blogcontent", formData.blogcontent); 100 | fromDataToSend.append("category", formData.category); 101 | 102 | if (formData.image) { 103 | fromDataToSend.append("file", formData.image); 104 | } 105 | 106 | try { 107 | const token = Cookies.get("token"); 108 | const { data } = await axios.post( 109 | `${author_service}/api/v1/blog/${id}`, 110 | fromDataToSend, 111 | { 112 | headers: { 113 | Authorization: `Bearer ${token}`, 114 | }, 115 | } 116 | ); 117 | 118 | toast.success(data.message); 119 | fetchBlogs(); 120 | } catch (error) { 121 | toast.error("Error while adding blog"); 122 | } finally { 123 | setLoading(false); 124 | } 125 | }; 126 | return ( 127 |
128 | 129 | 130 |

Add New Blog

131 |
132 | 133 |
134 | 135 |
136 | 143 |
144 | 145 | 146 |
147 | 154 |
155 | 156 | 157 | 175 | 176 |
177 | 178 | {existingImage && !formData.image && ( 179 | 184 | )} 185 | 186 |
187 | 188 |
189 | 190 |
191 |

192 | Paste you blog or type here. You can use rich text formatting. 193 | Please add image after improving your grammer 194 |

195 |
196 | { 202 | setContent(newContent); 203 | setFormData({ ...formData, blogcontent: newContent }); 204 | }} 205 | /> 206 |
207 | 208 | 211 |
212 |
213 |
214 |
215 | ); 216 | }; 217 | 218 | export default EditBlogPage; 219 | -------------------------------------------------------------------------------- /services/author/src/controllers/blog.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticatedRequest } from "../middlewares/isAuth.js"; 2 | import getBuffer from "../utils/dataUri.js"; 3 | import { sql } from "../utils/db.js"; 4 | import { invalidateChacheJob } from "../utils/rabbitmq.js"; 5 | import TryCatch from "../utils/TryCatch.js"; 6 | import cloudinary from "cloudinary"; 7 | import { GoogleGenAI } from "@google/genai"; 8 | import { GoogleGenerativeAI } from "@google/generative-ai"; 9 | 10 | export const createBlog = TryCatch(async (req: AuthenticatedRequest, res) => { 11 | const { title, description, blogcontent, category } = req.body; 12 | 13 | const file = req.file; 14 | 15 | if (!file) { 16 | res.status(400).json({ 17 | message: "No file to upload", 18 | }); 19 | return; 20 | } 21 | 22 | const fileBuffer = getBuffer(file); 23 | 24 | if (!fileBuffer || !fileBuffer.content) { 25 | res.status(400).json({ 26 | message: "Failed to generate buffer", 27 | }); 28 | return; 29 | } 30 | 31 | const cloud = await cloudinary.v2.uploader.upload(fileBuffer.content, { 32 | folder: "blogs", 33 | }); 34 | 35 | const result = 36 | await sql`INSERT INTO blogs (title, description, image, blogcontent,category, author) VALUES (${title}, ${description},${cloud.secure_url},${blogcontent},${category},${req.user?._id}) RETURNING *`; 37 | 38 | await invalidateChacheJob(["blogs:*"]); 39 | 40 | res.json({ 41 | message: "Blog Created", 42 | blog: result[0], 43 | }); 44 | }); 45 | 46 | export const updateBlog = TryCatch(async (req: AuthenticatedRequest, res) => { 47 | const { id } = req.params; 48 | const { title, description, blogcontent, category } = req.body; 49 | 50 | const file = req.file; 51 | 52 | const blog = await sql`SELECT * FROM blogs WHERE id = ${id}`; 53 | 54 | if (!blog.length) { 55 | res.status(404).json({ 56 | message: "No blog with this id", 57 | }); 58 | return; 59 | } 60 | 61 | if (blog[0].author !== req.user?._id) { 62 | res.status(401).json({ 63 | message: "You are not author of this blog", 64 | }); 65 | return; 66 | } 67 | 68 | let imageUrl = blog[0].image; 69 | 70 | if (file) { 71 | const fileBuffer = getBuffer(file); 72 | 73 | if (!fileBuffer || !fileBuffer.content) { 74 | res.status(400).json({ 75 | message: "Failed to generate buffer", 76 | }); 77 | return; 78 | } 79 | 80 | const cloud = await cloudinary.v2.uploader.upload(fileBuffer.content, { 81 | folder: "blogs", 82 | }); 83 | 84 | imageUrl = cloud.secure_url; 85 | } 86 | 87 | //Previous code 88 | 89 | /* const updatedBlog = await sql`UPDATE blogs SET 90 | title = ${title || blog[0].title}, 91 | description = ${title || blog[0].description}, 92 | image= ${imageUrl}, 93 | blogcontent = ${title || blog[0].blogcontent}, 94 | category = ${title || blog[0].category} 95 | 96 | WHERE id = ${id} 97 | RETURNING * 98 | `; */ 99 | 100 | //updated code 101 | 102 | const updatedBlog = await sql`UPDATE blogs SET 103 | title = ${title || blog[0].title}, 104 | description = ${description || blog[0].description}, 105 | image= ${imageUrl}, 106 | blogcontent = ${blogcontent || blog[0].blogcontent}, 107 | category = ${category || blog[0].category} 108 | 109 | WHERE id = ${id} 110 | RETURNING * 111 | `; 112 | 113 | await invalidateChacheJob(["blogs:*", `blog:${id}`]); 114 | 115 | res.json({ 116 | message: "Blog Updated", 117 | blog: updatedBlog[0], 118 | }); 119 | }); 120 | 121 | export const deleteBlog = TryCatch(async (req: AuthenticatedRequest, res) => { 122 | const blog = await sql`SELECT * FROM blogs WHERE id = ${req.params.id}`; 123 | 124 | if (!blog.length) { 125 | res.status(404).json({ 126 | message: "No blog with this id", 127 | }); 128 | return; 129 | } 130 | 131 | if (blog[0].author !== req.user?._id) { 132 | res.status(401).json({ 133 | message: "You are not author of this blog", 134 | }); 135 | return; 136 | } 137 | 138 | await sql`DELETE FROM savedblogs WHERE blogid = ${req.params.id}`; 139 | await sql`DELETE FROM comments WHERE blogid = ${req.params.id}`; 140 | await sql`DELETE FROM blogs WHERE id = ${req.params.id}`; 141 | 142 | await invalidateChacheJob(["blogs:*", `blog:${req.params.id}`]); 143 | 144 | res.json({ 145 | message: "Blog Delete", 146 | }); 147 | }); 148 | 149 | export const aiTitleResponse = TryCatch(async (req, res) => { 150 | const { text } = req.body; 151 | 152 | const prompt = `Correct the grammar of the following blog title and return only the corrected title without any additional text, formatting, or symbols: "${text}"`; 153 | 154 | let result; 155 | 156 | const ai = new GoogleGenAI({ 157 | apiKey: process.env.Gemini_Api_Key, 158 | }); 159 | 160 | async function main() { 161 | const response = await ai.models.generateContent({ 162 | model: "gemini-2.0-flash", 163 | contents: prompt, 164 | }); 165 | 166 | let rawtext = response.text; 167 | 168 | if (!rawtext) { 169 | res.status(400).json({ 170 | message: "Something went wrong", 171 | }); 172 | return; 173 | } 174 | 175 | result = rawtext 176 | .replace(/\*\*/g, "") 177 | .replace(/[\r\n]+/g, "") 178 | .replace(/[*_`~]/g, "") 179 | .trim(); 180 | } 181 | 182 | await main(); 183 | 184 | res.json(result); 185 | }); 186 | 187 | export const aiDescriptionResponse = TryCatch(async (req, res) => { 188 | const { title, description } = req.body; 189 | 190 | const prompt = 191 | description === "" 192 | ? `Generate only one short blog description based on this 193 | title: "${title}". Your response must be only one sentence, strictly under 30 words, with no options, no greetings, and 194 | no extra text. Do not explain. Do not say 'here is'. Just return the description only.` 195 | : `Fix the grammar in the 196 | following blog description and return only the corrected sentence. Do not add anything else: "${description}"`; 197 | 198 | let result; 199 | 200 | const ai = new GoogleGenAI({ 201 | apiKey: process.env.Gemini_Api_Key, 202 | }); 203 | 204 | async function main() { 205 | const response = await ai.models.generateContent({ 206 | model: "gemini-2.0-flash", 207 | contents: prompt, 208 | }); 209 | 210 | let rawtext = response.text; 211 | 212 | if (!rawtext) { 213 | res.status(400).json({ 214 | message: "Something went wrong", 215 | }); 216 | return; 217 | } 218 | 219 | result = rawtext 220 | .replace(/\*\*/g, "") 221 | .replace(/[\r\n]+/g, "") 222 | .replace(/[*_`~]/g, "") 223 | .trim(); 224 | } 225 | 226 | await main(); 227 | 228 | res.json(result); 229 | }); 230 | 231 | export const aiBlogResponse = TryCatch(async (req, res) => { 232 | const prompt = ` You will act as a grammar correction engine. I will provide you with blog content 233 | in rich HTML format (from Jodit Editor). Do not generate or rewrite the content with new ideas. Only correct 234 | grammatical, punctuation, and spelling errors while preserving all HTML tags and formatting. Maintain inline styles, 235 | image tags, line breaks, and structural tags exactly as they are. Return the full corrected HTML string as output. `; 236 | 237 | const { blog } = req.body; 238 | if (!blog) { 239 | res.status(400).json({ 240 | message: "Please provide blog", 241 | }); 242 | return; 243 | } 244 | 245 | const fullMessage = `${prompt}\n\n${blog}`; 246 | 247 | const ai = new GoogleGenerativeAI(process.env.Gemini_Api_Key as string); 248 | 249 | const model = ai.getGenerativeModel({ model: "gemini-1.5-pro" }); 250 | 251 | const result = await model.generateContent({ 252 | contents: [ 253 | { 254 | role: "user", 255 | parts: [ 256 | { 257 | text: fullMessage, 258 | }, 259 | ], 260 | }, 261 | ], 262 | }); 263 | 264 | const responseText = await result.response.text(); 265 | 266 | const cleanedHtml = responseText 267 | .replace(/^(html|```html|```)\n?/i, "") 268 | .replace(/```$/i, "") 269 | .replace(/\*\*/g, "") 270 | .replace(/[\r\n]+/g, "") 271 | .replace(/[*_`~]/g, "") 272 | .trim(); 273 | 274 | res.status(200).json({ html: cleanedHtml }); 275 | }); 276 | -------------------------------------------------------------------------------- /frontend/src/app/blog/new/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Button } from "@/components/ui/button"; 3 | import { Card, CardContent, CardHeader } from "@/components/ui/card"; 4 | import { Input } from "@/components/ui/input"; 5 | import { Label } from "@/components/ui/label"; 6 | import { 7 | Select, 8 | SelectContent, 9 | SelectItem, 10 | SelectTrigger, 11 | SelectValue, 12 | } from "@/components/ui/select"; 13 | import { RefreshCw } from "lucide-react"; 14 | import React, { useMemo, useRef, useState } from "react"; 15 | import dynamic from "next/dynamic"; 16 | import Cookies from "js-cookie"; 17 | import axios from "axios"; 18 | import { 19 | author_service, 20 | blogCategories, 21 | useAppData, 22 | } from "@/context/AppContext"; 23 | import toast from "react-hot-toast"; 24 | 25 | const JoditEditor = dynamic(() => import("jodit-react"), { ssr: false }); 26 | 27 | const AddBlog = () => { 28 | const editor = useRef(null); 29 | const [content, setContent] = useState(""); 30 | 31 | const { fetchBlogs } = useAppData(); 32 | 33 | const [loading, setLoading] = useState(false); 34 | const [formData, setFormData] = useState({ 35 | title: "", 36 | description: "", 37 | category: "", 38 | image: null, 39 | blogcontent: "", 40 | }); 41 | 42 | const handleInputChange = (e: any) => { 43 | setFormData({ ...formData, [e.target.name]: e.target.value }); 44 | }; 45 | 46 | const handleFileChange = (e: any) => { 47 | const file = e.target.files[0]; 48 | setFormData({ ...formData, image: file }); 49 | }; 50 | 51 | const handleSubmit = async (e: any) => { 52 | e.preventDefault(); 53 | setLoading(true); 54 | 55 | const fromDataToSend = new FormData(); 56 | 57 | fromDataToSend.append("title", formData.title); 58 | fromDataToSend.append("description", formData.description); 59 | fromDataToSend.append("blogcontent", formData.blogcontent); 60 | fromDataToSend.append("category", formData.category); 61 | 62 | if (formData.image) { 63 | fromDataToSend.append("file", formData.image); 64 | } 65 | 66 | try { 67 | const token = Cookies.get("token"); 68 | const { data } = await axios.post( 69 | `${author_service}/api/v1/blog/new`, 70 | fromDataToSend, 71 | { 72 | headers: { 73 | Authorization: `Bearer ${token}`, 74 | }, 75 | } 76 | ); 77 | 78 | toast.success(data.message); 79 | setFormData({ 80 | title: "", 81 | description: "", 82 | category: "", 83 | image: null, 84 | blogcontent: "", 85 | }); 86 | setContent(""); 87 | setTimeout(() => { 88 | fetchBlogs(); 89 | }, 4000); 90 | } catch (error) { 91 | toast.error("Error while adding blog"); 92 | } finally { 93 | setLoading(false); 94 | } 95 | }; 96 | 97 | const [aiTitle, setAiTitle] = useState(false); 98 | 99 | const aiTitleResponse = async () => { 100 | try { 101 | setAiTitle(true); 102 | const { data } = await axios.post(`${author_service}/api/v1/ai/title`, { 103 | text: formData.title, 104 | }); 105 | setFormData({ ...formData, title: data }); 106 | } catch (error) { 107 | toast.error("Problem while fetching from ai"); 108 | console.log(error); 109 | } finally { 110 | setAiTitle(false); 111 | } 112 | }; 113 | 114 | const [aiDescripiton, setAiDescription] = useState(false); 115 | 116 | const aiDescriptionResponse = async () => { 117 | try { 118 | setAiDescription(true); 119 | const { data } = await axios.post( 120 | `${author_service}/api/v1/ai/descripiton`, 121 | { 122 | title: formData.title, 123 | description: formData.description, 124 | } 125 | ); 126 | setFormData({ ...formData, description: data }); 127 | } catch (error) { 128 | toast.error("Problem while fetching from ai"); 129 | console.log(error); 130 | } finally { 131 | setAiDescription(false); 132 | } 133 | }; 134 | 135 | const [aiBlogLoading, setAiBlogLoading] = useState(false); 136 | 137 | const aiBlogResponse = async () => { 138 | try { 139 | setAiBlogLoading(true); 140 | const { data } = await axios.post(`${author_service}/api/v1/ai/blog`, { 141 | blog: formData.blogcontent, 142 | }); 143 | setContent(data.html); 144 | setFormData({ ...formData, blogcontent: data.html }); 145 | } catch (error: any) { 146 | toast.error("Problem while fetching from ai"); 147 | console.log(error); 148 | } finally { 149 | setAiBlogLoading(false); 150 | } 151 | }; 152 | 153 | const config = useMemo( 154 | () => ({ 155 | readonly: false, // all options from https://xdsoft.net/jodit/docs/, 156 | placeholder: "Start typings...", 157 | }), 158 | [] 159 | ); 160 | return ( 161 |
162 | 163 | 164 |

Add New Blog

165 |
166 | 167 |
168 | 169 |
170 | 180 | {formData.title === "" ? ( 181 | "" 182 | ) : ( 183 | 190 | )} 191 |
192 | 193 | 194 |
195 | 205 | {formData.title === "" ? ( 206 | "" 207 | ) : ( 208 | 215 | )} 216 |
217 | 218 | 219 | 237 | 238 |
239 | 240 | 241 |
242 | 243 |
244 | 245 |
246 |

247 | Paste you blog or type here. You can use rich text formatting. 248 | Please add image after improving your grammer 249 |

250 | 262 |
263 | { 269 | setContent(newContent); 270 | setFormData({ ...formData, blogcontent: newContent }); 271 | }} 272 | /> 273 |
274 | 275 | 278 |
279 |
280 |
281 |
282 | ); 283 | }; 284 | 285 | export default AddBlog; 286 | -------------------------------------------------------------------------------- /frontend/src/app/profile/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Avatar, AvatarImage } from "@/components/ui/avatar"; 3 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 4 | import { useAppData, user_service } from "@/context/AppContext"; 5 | import React, { useRef, useState } from "react"; 6 | import Cookies from "js-cookie"; 7 | import axios from "axios"; 8 | import toast from "react-hot-toast"; 9 | import Loading from "@/components/loading"; 10 | import { Facebook, Instagram, Linkedin } from "lucide-react"; 11 | import { Button } from "@/components/ui/button"; 12 | import { 13 | Dialog, 14 | DialogContent, 15 | DialogHeader, 16 | DialogTitle, 17 | DialogTrigger, 18 | } from "@/components/ui/dialog"; 19 | import { Label } from "@/components/ui/label"; 20 | import { Input } from "@/components/ui/input"; 21 | import { redirect, useRouter } from "next/navigation"; 22 | 23 | const ProfilePage = () => { 24 | const { user, setUser, logoutUser } = useAppData(); 25 | 26 | if (!user) return redirect("/login"); 27 | 28 | const logoutHandler = () => { 29 | logoutUser(); 30 | }; 31 | const InputRef = useRef(null); 32 | 33 | const [loading, setLoading] = useState(false); 34 | const [open, setOpen] = useState(false); 35 | const router = useRouter(); 36 | const [formData, setFormData] = useState({ 37 | name: user?.name || "", 38 | instagram: user?.instagram || "", 39 | facebook: user?.facebook || "", 40 | linkedin: user?.linkedin || "", 41 | bio: user?.bio || "", 42 | }); 43 | 44 | const clickHandler = () => { 45 | InputRef.current?.click(); 46 | }; 47 | 48 | const changeHandler = async (e: any) => { 49 | const file = e.target.files[0]; 50 | 51 | if (file) { 52 | const formData = new FormData(); 53 | 54 | formData.append("file", file); 55 | try { 56 | setLoading(true); 57 | const token = Cookies.get("token"); 58 | const { data } = await axios.post( 59 | `${user_service}/api/v1/user/update/pic`, 60 | formData, 61 | { 62 | headers: { 63 | Authorization: `Bearer ${token}`, 64 | }, 65 | } 66 | ); 67 | toast.success(data.message); 68 | setLoading(false); 69 | Cookies.set("token", data.token, { 70 | expires: 5, 71 | secure: true, 72 | path: "/", 73 | }); 74 | setUser(data.user); 75 | } catch (error) { 76 | toast.error("Image Update Failed"); 77 | setLoading(false); 78 | } 79 | } 80 | }; 81 | 82 | const handleFormSubmit = async () => { 83 | try { 84 | setLoading(true); 85 | const token = Cookies.get("token"); 86 | const { data } = await axios.post( 87 | `${user_service}/api/v1/user/update`, 88 | formData, 89 | { 90 | headers: { 91 | Authorization: `Bearer ${token}`, 92 | }, 93 | } 94 | ); 95 | 96 | toast.success(data.message); 97 | setLoading(false); 98 | Cookies.set("token", data.token, { 99 | expires: 5, 100 | secure: true, 101 | path: "/", 102 | }); 103 | setUser(data.user); 104 | setOpen(false); 105 | } catch (error) { 106 | toast.error("Update Failed"); 107 | setLoading(false); 108 | } 109 | }; 110 | 111 | return ( 112 |
113 | {loading ? ( 114 | 115 | ) : ( 116 | 117 | 118 | Profile 119 | 120 | 121 | 125 | 126 | 133 | 134 | 135 |
136 | 137 |

{user?.name}

138 |
139 | 140 | {user?.bio && ( 141 |
142 | 143 |

{user.bio}

144 |
145 | )} 146 | 147 |
148 | {user?.instagram && ( 149 | 154 | 155 | 156 | )} 157 | 158 | {user?.facebook && ( 159 | 164 | 165 | 166 | )} 167 | 168 | {user?.linkedin && ( 169 | 174 | 175 | 176 | )} 177 |
178 | 179 |
180 | 181 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | Edit Profile 192 | 193 | 194 |
195 |
196 | 197 | 200 | setFormData({ ...formData, name: e.target.value }) 201 | } 202 | /> 203 |
204 |
205 | 206 | 209 | setFormData({ ...formData, bio: e.target.value }) 210 | } 211 | /> 212 |
213 |
214 | 215 | 218 | setFormData({ 219 | ...formData, 220 | instagram: e.target.value, 221 | }) 222 | } 223 | /> 224 |
225 |
226 | 227 | 230 | setFormData({ 231 | ...formData, 232 | facebook: e.target.value, 233 | }) 234 | } 235 | /> 236 |
237 |
238 | 239 | 242 | setFormData({ 243 | ...formData, 244 | linkedin: e.target.value, 245 | }) 246 | } 247 | /> 248 |
249 | 250 | 256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 | )} 264 |
265 | ); 266 | }; 267 | 268 | export default ProfilePage; 269 | -------------------------------------------------------------------------------- /frontend/src/app/blog/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Loading from "@/components/loading"; 3 | import { Button } from "@/components/ui/button"; 4 | import { Card, CardContent, CardHeader } from "@/components/ui/card"; 5 | import { Input } from "@/components/ui/input"; 6 | import { Label } from "@/components/ui/label"; 7 | import { 8 | author_service, 9 | Blog, 10 | blog_service, 11 | useAppData, 12 | User, 13 | } from "@/context/AppContext"; 14 | import axios from "axios"; 15 | import { 16 | Bookmark, 17 | BookmarkCheck, 18 | Edit, 19 | Trash2, 20 | Trash2Icon, 21 | User2, 22 | } from "lucide-react"; 23 | import Link from "next/link"; 24 | import { useParams, useRouter } from "next/navigation"; 25 | import React, { useEffect, useState } from "react"; 26 | import Cookies from "js-cookie"; 27 | import toast from "react-hot-toast"; 28 | 29 | interface Comment { 30 | id: string; 31 | userid: string; 32 | comment: string; 33 | create_at: string; 34 | username: string; 35 | } 36 | 37 | const BlogPage = () => { 38 | const { isAuth, user, fetchBlogs, savedBlogs, getSavedBlogs } = useAppData(); 39 | const router = useRouter(); 40 | const { id } = useParams(); 41 | const [blog, setBlog] = useState(null); 42 | const [author, setAuthor] = useState(null); 43 | const [loading, setLoading] = useState(false); 44 | 45 | const [comments, setComments] = useState([]); 46 | 47 | async function fetchComment() { 48 | try { 49 | setLoading(true); 50 | const { data } = await axios.get(`${blog_service}/api/v1/comment/${id}`); 51 | setComments(data); 52 | } catch (error) { 53 | console.log(error); 54 | } finally { 55 | setLoading(false); 56 | } 57 | } 58 | 59 | useEffect(() => { 60 | fetchComment(); 61 | }, [id]); 62 | 63 | const [comment, setComment] = useState(""); 64 | 65 | async function addComment() { 66 | try { 67 | setLoading(true); 68 | const token = Cookies.get("token"); 69 | const { data } = await axios.post( 70 | `${blog_service}/api/v1/comment/${id}`, 71 | { comment }, 72 | { 73 | headers: { 74 | Authorization: `Bearer ${token}`, 75 | }, 76 | } 77 | ); 78 | toast.success(data.message); 79 | setComment(""); 80 | fetchComment(); 81 | } catch (error) { 82 | toast.error("Problem while adding comment"); 83 | } finally { 84 | setLoading(false); 85 | } 86 | } 87 | 88 | async function fetchSingleBlog() { 89 | try { 90 | setLoading(true); 91 | const { data } = await axios.get(`${blog_service}/api/v1/blog/${id}`); 92 | setBlog(data.blog); 93 | setAuthor(data.author); 94 | } catch (error) { 95 | console.log(error); 96 | } finally { 97 | setLoading(false); 98 | } 99 | } 100 | 101 | const deleteComment = async (id: string) => { 102 | if (confirm("Are you sure you want to delete this comment")) { 103 | try { 104 | setLoading(true); 105 | const token = Cookies.get("token"); 106 | const { data } = await axios.delete( 107 | `${blog_service}/api/v1/comment/${id}`, 108 | { 109 | headers: { 110 | Authorization: `Bearer ${token}`, 111 | }, 112 | } 113 | ); 114 | toast.success(data.message); 115 | fetchComment(); 116 | } catch (error) { 117 | toast.error("Problem while deleting comment"); 118 | console.log(error); 119 | } finally { 120 | setLoading(false); 121 | } 122 | } 123 | }; 124 | 125 | async function deletBlog() { 126 | if (confirm("Are you sure you want to delete this blog")) { 127 | try { 128 | setLoading(true); 129 | const token = Cookies.get("token"); 130 | const { data } = await axios.delete( 131 | `${author_service}/api/v1/blog/${id}`, 132 | { 133 | headers: { 134 | Authorization: `Bearer ${token}`, 135 | }, 136 | } 137 | ); 138 | toast.success(data.message); 139 | router.push("/blogs"); 140 | setTimeout(() => { 141 | fetchBlogs(); 142 | }, 4000); 143 | } catch (error) { 144 | toast.error("Problem while deleting comment"); 145 | console.log(error); 146 | } finally { 147 | setLoading(false); 148 | } 149 | } 150 | } 151 | 152 | const [saved, setSaved] = useState(false); 153 | 154 | useEffect(() => { 155 | if (savedBlogs && savedBlogs.some((b) => b.blogid === id)) { 156 | setSaved(true); 157 | } else { 158 | setSaved(false); 159 | } 160 | }, [savedBlogs, id]); 161 | 162 | async function saveBlog() { 163 | const token = Cookies.get("token"); 164 | try { 165 | setLoading(true); 166 | const { data } = await axios.post( 167 | `${blog_service}/api/v1/save/${id}`, 168 | {}, 169 | { 170 | headers: { 171 | Authorization: `Bearer ${token}`, 172 | }, 173 | } 174 | ); 175 | toast.success(data.message); 176 | setSaved(!saved); 177 | getSavedBlogs(); 178 | } catch (error) { 179 | toast.error("Problem while saving blog"); 180 | } finally { 181 | setLoading(false); 182 | } 183 | } 184 | 185 | useEffect(() => { 186 | fetchSingleBlog(); 187 | }, [id]); 188 | 189 | if (!blog) { 190 | return ; 191 | } 192 | 193 | return ( 194 |
195 | 196 | 197 |

{blog.title}

198 |

199 | 203 | 208 | {author?.name} 209 | 210 | {isAuth && ( 211 | 220 | )} 221 | {blog.author === user?._id && ( 222 | <> 223 | 229 | 238 | 239 | )} 240 |

241 |
242 | 243 | 248 |

{blog.description}

249 |
253 | 254 | 255 | 256 | {isAuth && ( 257 | 258 | 259 |

Leave a comment

260 |
261 | 262 | 263 | setComment(e.target.value)} 269 | /> 270 | 273 | 274 |
275 | )} 276 | 277 | 278 | 279 |

All Comments

280 |
281 | 282 | {comments && comments.length > 0 ? ( 283 | comments.map((e, i) => { 284 | return ( 285 |
286 |
287 |

288 | 289 | 290 | 291 | {e.username} 292 |

293 |

{e.comment}

294 |

295 | {new Date(e.create_at).toLocaleString()} 296 |

297 |
298 | {e.userid === user?._id && ( 299 | 306 | )} 307 |
308 | ); 309 | }) 310 | ) : ( 311 |

No Comments Yet

312 | )} 313 |
314 |
315 |
316 | ); 317 | }; 318 | 319 | export default BlogPage; 320 | -------------------------------------------------------------------------------- /frontend/src/components/ui/sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { Slot } from "@radix-ui/react-slot" 5 | import { VariantProps, cva } from "class-variance-authority" 6 | import { PanelLeftIcon } from "lucide-react" 7 | 8 | import { useIsMobile } from "@/hooks/use-mobile" 9 | import { cn } from "@/lib/utils" 10 | import { Button } from "@/components/ui/button" 11 | import { Input } from "@/components/ui/input" 12 | import { Separator } from "@/components/ui/separator" 13 | import { 14 | Sheet, 15 | SheetContent, 16 | SheetDescription, 17 | SheetHeader, 18 | SheetTitle, 19 | } from "@/components/ui/sheet" 20 | import { Skeleton } from "@/components/ui/skeleton" 21 | import { 22 | Tooltip, 23 | TooltipContent, 24 | TooltipProvider, 25 | TooltipTrigger, 26 | } from "@/components/ui/tooltip" 27 | 28 | const SIDEBAR_COOKIE_NAME = "sidebar_state" 29 | const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 30 | const SIDEBAR_WIDTH = "16rem" 31 | const SIDEBAR_WIDTH_MOBILE = "18rem" 32 | const SIDEBAR_WIDTH_ICON = "3rem" 33 | const SIDEBAR_KEYBOARD_SHORTCUT = "b" 34 | 35 | type SidebarContextProps = { 36 | state: "expanded" | "collapsed" 37 | open: boolean 38 | setOpen: (open: boolean) => void 39 | openMobile: boolean 40 | setOpenMobile: (open: boolean) => void 41 | isMobile: boolean 42 | toggleSidebar: () => void 43 | } 44 | 45 | const SidebarContext = React.createContext(null) 46 | 47 | function useSidebar() { 48 | const context = React.useContext(SidebarContext) 49 | if (!context) { 50 | throw new Error("useSidebar must be used within a SidebarProvider.") 51 | } 52 | 53 | return context 54 | } 55 | 56 | function SidebarProvider({ 57 | defaultOpen = true, 58 | open: openProp, 59 | onOpenChange: setOpenProp, 60 | className, 61 | style, 62 | children, 63 | ...props 64 | }: React.ComponentProps<"div"> & { 65 | defaultOpen?: boolean 66 | open?: boolean 67 | onOpenChange?: (open: boolean) => void 68 | }) { 69 | const isMobile = useIsMobile() 70 | const [openMobile, setOpenMobile] = React.useState(false) 71 | 72 | // This is the internal state of the sidebar. 73 | // We use openProp and setOpenProp for control from outside the component. 74 | const [_open, _setOpen] = React.useState(defaultOpen) 75 | const open = openProp ?? _open 76 | const setOpen = React.useCallback( 77 | (value: boolean | ((value: boolean) => boolean)) => { 78 | const openState = typeof value === "function" ? value(open) : value 79 | if (setOpenProp) { 80 | setOpenProp(openState) 81 | } else { 82 | _setOpen(openState) 83 | } 84 | 85 | // This sets the cookie to keep the sidebar state. 86 | document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` 87 | }, 88 | [setOpenProp, open] 89 | ) 90 | 91 | // Helper to toggle the sidebar. 92 | const toggleSidebar = React.useCallback(() => { 93 | return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) 94 | }, [isMobile, setOpen, setOpenMobile]) 95 | 96 | // Adds a keyboard shortcut to toggle the sidebar. 97 | React.useEffect(() => { 98 | const handleKeyDown = (event: KeyboardEvent) => { 99 | if ( 100 | event.key === SIDEBAR_KEYBOARD_SHORTCUT && 101 | (event.metaKey || event.ctrlKey) 102 | ) { 103 | event.preventDefault() 104 | toggleSidebar() 105 | } 106 | } 107 | 108 | window.addEventListener("keydown", handleKeyDown) 109 | return () => window.removeEventListener("keydown", handleKeyDown) 110 | }, [toggleSidebar]) 111 | 112 | // We add a state so that we can do data-state="expanded" or "collapsed". 113 | // This makes it easier to style the sidebar with Tailwind classes. 114 | const state = open ? "expanded" : "collapsed" 115 | 116 | const contextValue = React.useMemo( 117 | () => ({ 118 | state, 119 | open, 120 | setOpen, 121 | isMobile, 122 | openMobile, 123 | setOpenMobile, 124 | toggleSidebar, 125 | }), 126 | [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] 127 | ) 128 | 129 | return ( 130 | 131 | 132 |
147 | {children} 148 |
149 |
150 |
151 | ) 152 | } 153 | 154 | function Sidebar({ 155 | side = "left", 156 | variant = "sidebar", 157 | collapsible = "offcanvas", 158 | className, 159 | children, 160 | ...props 161 | }: React.ComponentProps<"div"> & { 162 | side?: "left" | "right" 163 | variant?: "sidebar" | "floating" | "inset" 164 | collapsible?: "offcanvas" | "icon" | "none" 165 | }) { 166 | const { isMobile, state, openMobile, setOpenMobile } = useSidebar() 167 | 168 | if (collapsible === "none") { 169 | return ( 170 |
178 | {children} 179 |
180 | ) 181 | } 182 | 183 | if (isMobile) { 184 | return ( 185 | 186 | 198 | 199 | Sidebar 200 | Displays the mobile sidebar. 201 | 202 |
{children}
203 |
204 |
205 | ) 206 | } 207 | 208 | return ( 209 |
217 | {/* This is what handles the sidebar gap on desktop */} 218 |
229 | 252 |
253 | ) 254 | } 255 | 256 | function SidebarTrigger({ 257 | className, 258 | onClick, 259 | ...props 260 | }: React.ComponentProps) { 261 | const { toggleSidebar } = useSidebar() 262 | 263 | return ( 264 | 279 | ) 280 | } 281 | 282 | function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { 283 | const { toggleSidebar } = useSidebar() 284 | 285 | return ( 286 |