From 8830809f80b98b7caae3fb69351afa67215de913 Mon Sep 17 00:00:00 2001 From: kaifmuhammad Date: Sat, 14 Jun 2025 15:43:19 +0500 Subject: [PATCH 1/6] FRONTEND_DOMAIN env added --- apps/api/src/routes/auth/google.js | 4 ++-- apps/api/src/routes/auth/login.js | 4 ++-- apps/api/src/routes/auth/logout.js | 4 ++-- apps/api/src/routes/auth/refresh.js | 2 +- apps/api/src/routes/auth/register.js | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/api/src/routes/auth/google.js b/apps/api/src/routes/auth/google.js index 01f0704..82a9acd 100644 --- a/apps/api/src/routes/auth/google.js +++ b/apps/api/src/routes/auth/google.js @@ -74,14 +74,14 @@ export async function completeOAuth(req, res) { secure: process.env.NODE_ENV === "production", maxAge: process.env.FINGERPRINT_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, sameSite: "lax", - domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_URL : "localhost" + domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_DOMAIN : "localhost" }); res.cookie("tokenrf", refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.REFRESH_TOKEN_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, sameSite: "lax", - domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_URL : "localhost" + domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_DOMAIN : "localhost" }); res.json({ message: "OAuth login completed successfully" }); diff --git a/apps/api/src/routes/auth/login.js b/apps/api/src/routes/auth/login.js index ec0887f..075c2df 100644 --- a/apps/api/src/routes/auth/login.js +++ b/apps/api/src/routes/auth/login.js @@ -32,8 +32,8 @@ export default async function login(req, res) { const refreshToken = generateRefreshToken(user._id, fingerprint, `${process.env.REFRESH_TOKEN_EXPIRATION_DAYS || 7}d`) res.header("Authorization", `Bearer ${token}`) - res.cookie("securefp", fingerprint, { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.FINGERPRINT_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, sameSite: "lax", domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_URL : "localhost" }); - res.cookie("tokenrf", refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.REFRESH_TOKEN_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, sameSite: "lax", domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_URL : "localhost" }); + res.cookie("securefp", fingerprint, { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.FINGERPRINT_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, sameSite: "lax", domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_DOMAIN : "localhost" }); + res.cookie("tokenrf", refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.REFRESH_TOKEN_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, sameSite: "lax", domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_DOMAIN : "localhost" }); res.json({message: "Logged in successfully"}); } \ No newline at end of file diff --git a/apps/api/src/routes/auth/logout.js b/apps/api/src/routes/auth/logout.js index 7534b53..aa6e475 100644 --- a/apps/api/src/routes/auth/logout.js +++ b/apps/api/src/routes/auth/logout.js @@ -6,7 +6,7 @@ async function logout(req, res) { secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', - domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_URL : "localhost" + domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_DOMAIN : "localhost" }) res.cookie("securefp", "", { maxAge: 0, @@ -15,7 +15,7 @@ async function logout(req, res) { secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', - domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_URL : "localhost" + domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_DOMAIN : "localhost" }) return res.status(200).send("Logged out successfully"); } diff --git a/apps/api/src/routes/auth/refresh.js b/apps/api/src/routes/auth/refresh.js index c1430ad..fb4bc71 100644 --- a/apps/api/src/routes/auth/refresh.js +++ b/apps/api/src/routes/auth/refresh.js @@ -17,7 +17,7 @@ import { generateAuthToken, generateRefreshToken } from "../../services/tokens.j secure: process.env.NODE_ENV === "production", maxAge: process.env.REFRESH_TOKEN_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, sameSite: "lax", - domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_URL : "localhost" }); + domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_DOMAIN : "localhost" }); // res.cookie("securefp", fingerprint, { // httpOnly: true, // secure: process.env.NODE_ENV === "production", diff --git a/apps/api/src/routes/auth/register.js b/apps/api/src/routes/auth/register.js index 4c790bc..6cc2368 100644 --- a/apps/api/src/routes/auth/register.js +++ b/apps/api/src/routes/auth/register.js @@ -30,14 +30,14 @@ export default async function register(req, res) { secure: process.env.NODE_ENV === "production", maxAge: process.env.FINGERPRINT_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, sameSite: "lax", - domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_URL : "localhost" + domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_DOMAIN : "localhost" }); res.cookie("tokenrf", refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.REFRESH_TOKEN_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, sameSite: "lax", - domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_URL : "localhost" + domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_DOMAIN : "localhost" }); res.json({ name: user.name, email: user.email}); From 563e699b90ea2034f9d26b35509d1adb81da04ae Mon Sep 17 00:00:00 2001 From: kaifmuhammad Date: Sat, 14 Jun 2025 15:55:15 +0500 Subject: [PATCH 2/6] Domain attribute was removed from cookie Backend and frontend deployment is not on same domain --- apps/api/src/routes/auth/google.js | 6 ++---- apps/api/src/routes/auth/login.js | 4 ++-- apps/api/src/routes/auth/logout.js | 6 ++---- apps/api/src/routes/auth/refresh.js | 3 +-- apps/api/src/routes/auth/register.js | 6 ++---- 5 files changed, 9 insertions(+), 16 deletions(-) diff --git a/apps/api/src/routes/auth/google.js b/apps/api/src/routes/auth/google.js index 82a9acd..a95dc42 100644 --- a/apps/api/src/routes/auth/google.js +++ b/apps/api/src/routes/auth/google.js @@ -73,15 +73,13 @@ export async function completeOAuth(req, res) { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.FINGERPRINT_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, - sameSite: "lax", - domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_DOMAIN : "localhost" + sameSite: "lax" }); res.cookie("tokenrf", refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.REFRESH_TOKEN_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, - sameSite: "lax", - domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_DOMAIN : "localhost" + sameSite: "lax" }); res.json({ message: "OAuth login completed successfully" }); diff --git a/apps/api/src/routes/auth/login.js b/apps/api/src/routes/auth/login.js index 075c2df..963643e 100644 --- a/apps/api/src/routes/auth/login.js +++ b/apps/api/src/routes/auth/login.js @@ -32,8 +32,8 @@ export default async function login(req, res) { const refreshToken = generateRefreshToken(user._id, fingerprint, `${process.env.REFRESH_TOKEN_EXPIRATION_DAYS || 7}d`) res.header("Authorization", `Bearer ${token}`) - res.cookie("securefp", fingerprint, { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.FINGERPRINT_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, sameSite: "lax", domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_DOMAIN : "localhost" }); - res.cookie("tokenrf", refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.REFRESH_TOKEN_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, sameSite: "lax", domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_DOMAIN : "localhost" }); + res.cookie("securefp", fingerprint, { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.FINGERPRINT_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, sameSite: "lax" }); + res.cookie("tokenrf", refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.REFRESH_TOKEN_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, sameSite: "lax" }); res.json({message: "Logged in successfully"}); } \ No newline at end of file diff --git a/apps/api/src/routes/auth/logout.js b/apps/api/src/routes/auth/logout.js index aa6e475..21116ef 100644 --- a/apps/api/src/routes/auth/logout.js +++ b/apps/api/src/routes/auth/logout.js @@ -5,8 +5,7 @@ async function logout(req, res) { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', - path: '/', - domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_DOMAIN : "localhost" + path: '/' }) res.cookie("securefp", "", { maxAge: 0, @@ -14,8 +13,7 @@ async function logout(req, res) { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', - path: '/', - domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_DOMAIN : "localhost" + path: '/' }) return res.status(200).send("Logged out successfully"); } diff --git a/apps/api/src/routes/auth/refresh.js b/apps/api/src/routes/auth/refresh.js index fb4bc71..3a5e0e2 100644 --- a/apps/api/src/routes/auth/refresh.js +++ b/apps/api/src/routes/auth/refresh.js @@ -16,8 +16,7 @@ import { generateAuthToken, generateRefreshToken } from "../../services/tokens.j httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.REFRESH_TOKEN_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, - sameSite: "lax", - domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_DOMAIN : "localhost" }); + sameSite: "lax" }); // res.cookie("securefp", fingerprint, { // httpOnly: true, // secure: process.env.NODE_ENV === "production", diff --git a/apps/api/src/routes/auth/register.js b/apps/api/src/routes/auth/register.js index 6cc2368..a69acc8 100644 --- a/apps/api/src/routes/auth/register.js +++ b/apps/api/src/routes/auth/register.js @@ -29,15 +29,13 @@ export default async function register(req, res) { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.FINGERPRINT_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, - sameSite: "lax", - domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_DOMAIN : "localhost" + sameSite: "lax" }); res.cookie("tokenrf", refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.REFRESH_TOKEN_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, - sameSite: "lax", - domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_DOMAIN : "localhost" + sameSite: "lax" }); res.json({ name: user.name, email: user.email}); From 33dc245330d6030cb34a01c907510e9d2b9ebb65 Mon Sep 17 00:00:00 2001 From: kaifmuhammad Date: Sat, 14 Jun 2025 15:58:34 +0500 Subject: [PATCH 3/6] Samesite attributed set to none Samesite attributed set to none because backend and frontend are not under same domain --- apps/api/src/routes/auth/google.js | 4 ++-- apps/api/src/routes/auth/login.js | 4 ++-- apps/api/src/routes/auth/logout.js | 4 ++-- apps/api/src/routes/auth/refresh.js | 2 +- apps/api/src/routes/auth/register.js | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/api/src/routes/auth/google.js b/apps/api/src/routes/auth/google.js index a95dc42..1bc39e5 100644 --- a/apps/api/src/routes/auth/google.js +++ b/apps/api/src/routes/auth/google.js @@ -73,13 +73,13 @@ export async function completeOAuth(req, res) { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.FINGERPRINT_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, - sameSite: "lax" + sameSite: "none" }); res.cookie("tokenrf", refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.REFRESH_TOKEN_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, - sameSite: "lax" + sameSite: "none" }); res.json({ message: "OAuth login completed successfully" }); diff --git a/apps/api/src/routes/auth/login.js b/apps/api/src/routes/auth/login.js index 963643e..f0435a4 100644 --- a/apps/api/src/routes/auth/login.js +++ b/apps/api/src/routes/auth/login.js @@ -32,8 +32,8 @@ export default async function login(req, res) { const refreshToken = generateRefreshToken(user._id, fingerprint, `${process.env.REFRESH_TOKEN_EXPIRATION_DAYS || 7}d`) res.header("Authorization", `Bearer ${token}`) - res.cookie("securefp", fingerprint, { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.FINGERPRINT_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, sameSite: "lax" }); - res.cookie("tokenrf", refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.REFRESH_TOKEN_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, sameSite: "lax" }); + res.cookie("securefp", fingerprint, { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.FINGERPRINT_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, sameSite: "none" }); + res.cookie("tokenrf", refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.REFRESH_TOKEN_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, sameSite: "none" }); res.json({message: "Logged in successfully"}); } \ No newline at end of file diff --git a/apps/api/src/routes/auth/logout.js b/apps/api/src/routes/auth/logout.js index 21116ef..0519166 100644 --- a/apps/api/src/routes/auth/logout.js +++ b/apps/api/src/routes/auth/logout.js @@ -4,7 +4,7 @@ async function logout(req, res) { expires: new Date(0), httpOnly: true, secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', + sameSite: 'none', path: '/' }) res.cookie("securefp", "", { @@ -12,7 +12,7 @@ async function logout(req, res) { expires: new Date(0), httpOnly: true, secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', + sameSite: 'none', path: '/' }) return res.status(200).send("Logged out successfully"); diff --git a/apps/api/src/routes/auth/refresh.js b/apps/api/src/routes/auth/refresh.js index 3a5e0e2..f2e3f9c 100644 --- a/apps/api/src/routes/auth/refresh.js +++ b/apps/api/src/routes/auth/refresh.js @@ -16,7 +16,7 @@ import { generateAuthToken, generateRefreshToken } from "../../services/tokens.j httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.REFRESH_TOKEN_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, - sameSite: "lax" }); + sameSite: "none" }); // res.cookie("securefp", fingerprint, { // httpOnly: true, // secure: process.env.NODE_ENV === "production", diff --git a/apps/api/src/routes/auth/register.js b/apps/api/src/routes/auth/register.js index a69acc8..e2bd0f4 100644 --- a/apps/api/src/routes/auth/register.js +++ b/apps/api/src/routes/auth/register.js @@ -29,13 +29,13 @@ export default async function register(req, res) { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.FINGERPRINT_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, - sameSite: "lax" + sameSite: "none" }); res.cookie("tokenrf", refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: process.env.REFRESH_TOKEN_EXPIRATION_DAYS * 24 * 60 * 60 * 1000, - sameSite: "lax" + sameSite: "none" }); res.json({ name: user.name, email: user.email}); From ae2c9b16ef2cc66934a932301d8ba4de082899f3 Mon Sep 17 00:00:00 2001 From: Tanveer Hamza <152054823+TanveerHamza@users.noreply.github.com> Date: Mon, 16 Jun 2025 11:46:05 +0500 Subject: [PATCH 4/6] generate posts from idea and content source --- apps/api/src/routes/auth/register.js | 9 +- apps/api/src/routes/posts/delete.js | 23 +- apps/api/src/routes/posts/get-posts.js | 74 ++-- apps/api/src/routes/posts/get-single-post.js | 38 ++ apps/api/src/routes/posts/index.js | 2 + .../components/ideas/GeneratePostsModal.tsx | 253 ++++++++++++ .../src/components/ideas/IdeaCard.tsx | 25 +- apps/frontend/src/components/posts/card.tsx | 57 ++- .../src/components/posts/edit-post-modal.tsx | 387 ++++++++---------- .../components/sources/ContentViewModal.tsx | 4 +- .../components/sources/GeneratePostsModal.tsx | 260 ++++++++++++ apps/frontend/src/components/sources/List.tsx | 39 +- .../frontend/src/hooks/useGenerateFromIdea.ts | 30 ++ .../src/hooks/useGenerateFromSource.ts | 28 ++ apps/frontend/src/routeTree.gen.ts | 50 ++- .../src/routes/_sidebarLayout/ideas.tsx | 15 + .../routes/_sidebarLayout/posts.$postId.tsx | 258 ++++++++++++ .../src/routes/_sidebarLayout/posts.tsx | 45 +- .../src/routes/_sidebarLayout/sources.tsx | 19 +- apps/frontend/src/routes/index.tsx | 42 +- apps/frontend/src/types/content.ts | 1 + apps/frontend/src/utils/store.ts | 26 +- dnjsna | 10 + 23 files changed, 1371 insertions(+), 324 deletions(-) create mode 100644 apps/api/src/routes/posts/get-single-post.js create mode 100644 apps/frontend/src/components/ideas/GeneratePostsModal.tsx create mode 100644 apps/frontend/src/components/sources/GeneratePostsModal.tsx create mode 100644 apps/frontend/src/hooks/useGenerateFromIdea.ts create mode 100644 apps/frontend/src/hooks/useGenerateFromSource.ts create mode 100644 apps/frontend/src/routes/_sidebarLayout/posts.$postId.tsx create mode 100644 dnjsna diff --git a/apps/api/src/routes/auth/register.js b/apps/api/src/routes/auth/register.js index c1fd3ba..a410d91 100644 --- a/apps/api/src/routes/auth/register.js +++ b/apps/api/src/routes/auth/register.js @@ -38,6 +38,13 @@ export default async function register(req, res) { sameSite: "none" }); - res.status(201).json({ name: user.name, email: user.email, id: user._id.toString()}); + res.status(201).json({ + message: "User registered successfully", + user: { + id: user._id.toString(), + name: user.name, + email: user.email + } + }); }; diff --git a/apps/api/src/routes/posts/delete.js b/apps/api/src/routes/posts/delete.js index 49b57e6..4654e83 100644 --- a/apps/api/src/routes/posts/delete.js +++ b/apps/api/src/routes/posts/delete.js @@ -1,21 +1,36 @@ import Content from "../../models/Content.js"; +import { Idea } from "../../models/Idea.js"; import mongoose from "mongoose"; async function deletePost(req, res) { try { const { postId } = req.params; const user = req.user; - const result = await Content.findOneAndUpdate( + + // Try to delete from Content first + const contentResult = await Content.findOneAndUpdate( + { "posts._id": postId, user: user }, + { $pull: { posts: { _id: postId } } }, + { new: true } + ); + + if (contentResult) { + return res.status(200).json({ message: "Post deleted successfully" }); + } + + // If not found in Content, try to delete from Ideas + const ideaResult = await Idea.findOneAndUpdate( { "posts._id": postId, user: user }, { $pull: { posts: { _id: postId } } }, { new: true } ); - if (!result) { - return res.status(404).json({ error: "Post not found" }); + if (ideaResult) { + return res.status(200).json({ message: "Post deleted successfully" }); } - res.status(200).json({ message: "Post deleted successfully" }); + // If not found in either collection + return res.status(404).json({ error: "Post not found" }); } catch (error) { console.error("Error deleting post:", error); res.status(500).json({ error: "Failed to delete post" }); diff --git a/apps/api/src/routes/posts/get-posts.js b/apps/api/src/routes/posts/get-posts.js index 79b6708..119d3ef 100644 --- a/apps/api/src/routes/posts/get-posts.js +++ b/apps/api/src/routes/posts/get-posts.js @@ -1,5 +1,6 @@ import mongoose from "mongoose"; import Content from "../../models/Content.js"; +import { Idea } from "../../models/Idea.js"; async function getPosts(req, res) { const page = parseInt(req.query.page) || 1; @@ -7,37 +8,36 @@ async function getPosts(req, res) { const skip = (page - 1) * limit; try { - const countPipeline = [ + // Get posts from Content sources + const contentPostsPipeline = [ { $match: { user: mongoose.Types.ObjectId.createFromHexString(req.user) }, }, { $unwind: "$posts" }, - { $count: "total" }, - ]; - - const countResult = await Content.aggregate(countPipeline); - const total = countResult.length > 0 ? countResult[0].total : 0; - - if (total === 0) { - return res.json({ - data: [], - pagination: { - total: 0, - page, - limit, - totalPages: 0, + { + $project: { + _id: "$posts._id", + title: "$posts.title", + description: "$posts.description", + platform: "$posts.platform", + tags: "$posts.tags", + length: "$posts.length", + customLength: "$posts.customLength", + tone: "$posts.tone", + createdAt: "$posts.createdAt", + sourceTitle: "$label", + sourceId: "$_id", + sourceType: "content", }, - }); - } + }, + ]; - const pipeline = [ + // Get posts from Ideas + const ideaPostsPipeline = [ { $match: { user: mongoose.Types.ObjectId.createFromHexString(req.user) }, }, { $unwind: "$posts" }, - { $sort: { "posts.createdAt": -1 } }, - { $skip: skip }, - { $limit: limit }, { $project: { _id: "$posts._id", @@ -49,13 +49,41 @@ async function getPosts(req, res) { customLength: "$posts.customLength", tone: "$posts.tone", createdAt: "$posts.createdAt", - sourceTitle: "$label", + sourceTitle: "$title", sourceId: "$_id", + sourceType: "idea", }, }, ]; - const paginatedPosts = await Content.aggregate(pipeline); + // Execute both pipelines + const [contentPosts, ideaPosts] = await Promise.all([ + Content.aggregate(contentPostsPipeline), + Idea.aggregate(ideaPostsPipeline), + ]); + + // Combine and sort all posts by creation date + const allPosts = [...contentPosts, ...ideaPosts].sort( + (a, b) => new Date(b.createdAt) - new Date(a.createdAt) + ); + + const total = allPosts.length; + + if (total === 0) { + return res.json({ + data: [], + pagination: { + total: 0, + page, + limit, + totalPages: 0, + }, + }); + } + + // Apply pagination + const paginatedPosts = allPosts.slice(skip, skip + limit); + res.json({ data: paginatedPosts, pagination: { diff --git a/apps/api/src/routes/posts/get-single-post.js b/apps/api/src/routes/posts/get-single-post.js new file mode 100644 index 0000000..557921a --- /dev/null +++ b/apps/api/src/routes/posts/get-single-post.js @@ -0,0 +1,38 @@ +import Post from "../../models/Post.js"; + +const getSinglePost = async (req, res) => { + try { + const { postId } = req.params; + + if (!postId) { + return res.status(400).json({ + success: false, + message: "Post ID is required" + }); + } + + // Find the post by ID + const post = await Post.findById(postId); + + if (!post) { + return res.status(404).json({ + success: false, + message: "Post not found" + }); + } + + res.status(200).json({ + success: true, + post: post + }); + + } catch (error) { + console.error("Error fetching post:", error); + res.status(500).json({ + success: false, + message: "Failed to fetch post" + }); + } +}; + +export default getSinglePost; \ No newline at end of file diff --git a/apps/api/src/routes/posts/index.js b/apps/api/src/routes/posts/index.js index d492994..a118ec1 100644 --- a/apps/api/src/routes/posts/index.js +++ b/apps/api/src/routes/posts/index.js @@ -1,5 +1,6 @@ import express from "express"; import getPosts from "./get-posts.js"; +import getSinglePost from "./get-single-post.js"; import generate from "./generate.js"; import generateFromIdea from "./generate-from-idea.js"; import deletePost from "./delete.js"; @@ -8,6 +9,7 @@ import modify from "./modify.js"; const router = express.Router(); router.get("/", getPosts); +router.get("/:postId", getSinglePost); router.post("/generate", generate); router.post("/generate-from-idea", generateFromIdea); router.delete("/:postId", deletePost); diff --git a/apps/frontend/src/components/ideas/GeneratePostsModal.tsx b/apps/frontend/src/components/ideas/GeneratePostsModal.tsx new file mode 100644 index 0000000..b545cca --- /dev/null +++ b/apps/frontend/src/components/ideas/GeneratePostsModal.tsx @@ -0,0 +1,253 @@ +import { useState } from "react"; +import { X, Sparkles, Linkedin, Twitter, Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Slider } from "@/components/ui/slider"; +import { Idea } from "@/types/idea"; +import { useGenerateFromIdea } from "@/hooks/useGenerateFromIdea"; +import toast from "react-hot-toast"; +import { useNavigate } from "@tanstack/react-router"; + +interface GeneratePostsModalProps { + idea: Idea; + onClose: () => void; +} + +export function GeneratePostsModal({ idea, onClose }: GeneratePostsModalProps) { + const [count, setCount] = useState(3); + const [platform, setPlatform] = useState<"linkedin" | "x" | "both">("both"); + const [tone, setTone] = useState<"professional" | "narrative" | "informative" | "persuasive" | "casual" | "formal" | "neutral">("professional"); + const [length, setLength] = useState<"short" | "medium" | "long" | "custom">("medium"); + const [customLength, setCustomLength] = useState(200); + + const { mutateAsync: generatePosts, isPending } = useGenerateFromIdea(); + const navigate = useNavigate(); + + const platforms = [ + { + id: "linkedin" as const, + label: "LinkedIn", + icon: , + color: "bg-[#0077B5]", + description: "Professional network for B2B content", + }, + { + id: "x" as const, + label: "X (Twitter)", + icon: , + color: "bg-black", + description: "Fast-paced platform for concise updates", + }, + { + id: "both" as const, + label: "Both Platforms", + icon: ( +
+ + +
+
+ ), + color: "bg-gradient-to-br from-[#0077B5] to-black", + description: "Optimized for both platforms", + }, + ]; + + const tones = [ + { id: "professional" as const, name: "Professional", description: "Formal and business-like" }, + { id: "narrative" as const, name: "Narrative", description: "Storytelling approach" }, + { id: "informative" as const, name: "Informative", description: "Educational and detailed" }, + { id: "persuasive" as const, name: "Persuasive", description: "Convincing and influential" }, + { id: "casual" as const, name: "Casual", description: "Relaxed and conversational" }, + { id: "formal" as const, name: "Formal", description: "Structured and traditional" }, + { id: "neutral" as const, name: "Neutral", description: "Balanced and impartial" }, + ]; + + const lengthOptions = [ + { id: "short" as const, label: "Short", description: "50-100 words" }, + { id: "medium" as const, label: "Medium", description: "100-200 words" }, + { id: "long" as const, label: "Long", description: "200-300 words" }, + { id: "custom" as const, label: "Custom", description: "Specify word count" }, + ]; + + const handleGenerate = async () => { + try { + const requestData = { + ideaId: idea._id, + count, + platform, + tone, + ...(length === "custom" ? { customLength } : { length }), + }; + + await generatePosts(requestData); + + toast.success(`Successfully generated ${count} post${count > 1 ? 's' : ''} from "${idea.title}"!`); + onClose(); + + // Navigate to posts page to see the generated posts + navigate({ to: "/posts" }); + } catch (error) { + toast.error("Failed to generate posts. Please try again."); + console.error("Generate posts error:", error); + } + }; + + return ( +
+
+ {/* Header */} +
+
+

Generate Posts

+

From: {idea.title}

+
+ +
+ +
+ {/* Post Count */} +
+ + setCount(value[0])} + max={10} + min={1} + step={1} + className="w-full" + /> +
+ 1 + 10 +
+
+ + {/* Platform Selection */} +
+ +
+ {platforms.map((p) => ( + + ))} +
+
+ + {/* Tone Selection */} +
+ +
+ {tones.map((t) => ( + + ))} +
+
+ + {/* Length Selection */} +
+ +
+ {lengthOptions.map((l) => ( + + ))} +
+ + {length === "custom" && ( +
+ + setCustomLength(value[0])} + max={1000} + min={100} + step={50} + className="w-full" + /> +
+ 100 + 1000 +
+
+ )} +
+
+ + {/* Footer */} +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/frontend/src/components/ideas/IdeaCard.tsx b/apps/frontend/src/components/ideas/IdeaCard.tsx index 5258109..c47eada 100644 --- a/apps/frontend/src/components/ideas/IdeaCard.tsx +++ b/apps/frontend/src/components/ideas/IdeaCard.tsx @@ -8,6 +8,7 @@ interface ContentIdeaCardProps { isSelected: boolean; onSelect: () => void; onDelete?: (ideaId: string) => void; + onGeneratePosts?: (idea: Idea) => void; selectionMode?: boolean; } @@ -16,6 +17,7 @@ export function ContentIdeaCard({ isSelected, onSelect, onDelete, + onGeneratePosts, selectionMode = false, }: ContentIdeaCardProps) { const [isHovered, setIsHovered] = useState(false); @@ -90,17 +92,18 @@ export function ContentIdeaCard({ )} - + {onGeneratePosts && ( + + )} diff --git a/apps/frontend/src/components/posts/card.tsx b/apps/frontend/src/components/posts/card.tsx index f4bc989..4d44b0b 100644 --- a/apps/frontend/src/components/posts/card.tsx +++ b/apps/frontend/src/components/posts/card.tsx @@ -82,6 +82,17 @@ export default function PostCard({ Draft + {post.sourceType && ( + + {post.sourceType === "content" ? "From Sources" : "From Ideas"} + + )} {onDiscard && ( -
Edit Post
- -
- - - + +
-
+ + {/* Error Alert */} {isSubmitted && Object.keys(errors).length > 0 && ( - - - -
    - {renderErrorMessages()} -
-
-
+
+ + + +
    + {renderErrorMessages()} +
+
+
+
)} -
-
- {initialData.sourceTitle} -
-
- Draft -
-
+
+
+ {/* Post Content Section */} +
+
+
+ ✏️ +
+

Content

+
-
- {/* Left Column - Post Content */} -
-
- {watch("platform")} -
+ {/* Title Input */} +
+
-
-
- - - - - + {/* Rich Text Toolbar */} +
+ + + + +
+ + {/* Description Textarea */} +
+ +