diff --git a/backend/database/models/postModel.js b/backend/database/models/postModel.js index e09c362..3d3f29c 100644 --- a/backend/database/models/postModel.js +++ b/backend/database/models/postModel.js @@ -2,19 +2,12 @@ import { Schema, model } from "mongoose"; const postModel = new Schema( { - postID: String, - userID: { type: String, ref: "users" }, - postTitle: String, - image_id: String, - content: String, - likes: { - type: [String], // array of user ids - default: [], - }, - comments: { - type: [String], - default: [], - }, + createdBy: { type: String, ref: "users" }, + title: { type: String, required: true }, + content: { type: String, required: true }, + upvote: { type: Number, default: 0 }, + comments: [{ userId: String, comment: String }], + imageUrl: { type: String, default: "" }, }, { timestamps: true } ); @@ -32,7 +25,7 @@ export async function findAll() { export async function findById(id) { const data = await post.findOne({ - postID: { $eq: id }, + _id: { $eq: id }, }); if (!data) return null; return data; @@ -40,16 +33,16 @@ export async function findById(id) { export async function getPostbyTitle(postTitle) { const data = await post.findOne({ - postTitle: { $eq: postTitle }, + title: { $eq: postTitle }, }); if (!data) return null; return data; } export async function update(id, data) { - return await post.updateOne({ postID: id }, data); + return await post.updateOne({ _id: id }, data); } export async function findByIdAndDelete(id) { - return await post.deleteOne({ postID: id }); + return await post.deleteOne({ _id: id }); } diff --git a/backend/routes/api.post.js b/backend/routes/api.post.js index 43f925d..dded946 100644 --- a/backend/routes/api.post.js +++ b/backend/routes/api.post.js @@ -21,6 +21,16 @@ router.get('/', async (req, res) => { } }) +router.get("/:id", async (req, res) => { + try { + const id = req.params.id + const post = await findById(id) + res.status(200).json(post) + } catch (error) { + res.status(500).send({ message: error.message }); + } +}) + router.get('/:title', async (req, res) => { try { const title = req.params.title @@ -79,14 +89,18 @@ router.put('/:id/likePost', async (req, res) => { router.put('/:id/commentPost', async (req, res) => { try { - const id = req.params + const id = req.params.id const value = req.body + console.log("id", id) + console.log("value", value) - const post = await findById(id) + let post = await findById(id) + if(!post) return res.status(404).json({ message: 'Post not found' }); + console.log(value) post.comments.push(value) - const updatedPost = await update(id, post) - - res.json(updatedPost); + console.log(post) + post = await update(id, post) + res.json(post); } catch (error) { res.status(500).send({ message: error.message }); } diff --git a/frontend/public/dashboard/post.svg b/frontend/public/dashboard/post.svg new file mode 100644 index 0000000..880817a --- /dev/null +++ b/frontend/public/dashboard/post.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/app/actions/addFriend-action.ts b/frontend/src/app/actions/addFriend-action.ts index ce6ee32..53fef20 100644 --- a/frontend/src/app/actions/addFriend-action.ts +++ b/frontend/src/app/actions/addFriend-action.ts @@ -7,7 +7,6 @@ export default async function addFriendAction(userId: string, friendId: string) method: "POST", headers: { "Content-Type": "application/json", - "Cache-Control": "max-age=3600", }, body: JSON.stringify({ userId, friendId }), } diff --git a/frontend/src/app/actions/deleteRoom-action.ts b/frontend/src/app/actions/deleteRoom-action.ts index 189898e..21888cb 100644 --- a/frontend/src/app/actions/deleteRoom-action.ts +++ b/frontend/src/app/actions/deleteRoom-action.ts @@ -7,7 +7,6 @@ export default async function deleteRoomAction(id: string) { const url = `${process.env.API_URL}/api/rooms/${id}` const response = await fetch(url, { method: "DELETE", - next: { tags: ["delete-rooms"] }, headers: { "Content-Type": "application/json" } diff --git a/frontend/src/app/actions/getUserFriend-action.ts b/frontend/src/app/actions/getUserFriend-action.ts index afb08de..9566b7c 100644 --- a/frontend/src/app/actions/getUserFriend-action.ts +++ b/frontend/src/app/actions/getUserFriend-action.ts @@ -4,7 +4,6 @@ export default async function getUserFriend(username: string) { `${process.env.API_URL}/api/users/${username}/friends`, { method: "GET", - cache: "force-cache", next: { tags: ["friends"] }, headers: { "Content-Type": "application/json", diff --git a/frontend/src/app/actions/getUserRoom-action.ts b/frontend/src/app/actions/getUserRoom-action.ts index 24238f4..5912d09 100644 --- a/frontend/src/app/actions/getUserRoom-action.ts +++ b/frontend/src/app/actions/getUserRoom-action.ts @@ -8,7 +8,6 @@ export async function getUserRoom(id: string): Promise { { method: "GET", next: { tags: ["rooms"] }, - cache: "force-cache", headers: { "Content-Type": "application/json", }, diff --git a/frontend/src/app/actions/post-action.ts b/frontend/src/app/actions/post-action.ts new file mode 100644 index 0000000..5e3e090 --- /dev/null +++ b/frontend/src/app/actions/post-action.ts @@ -0,0 +1,97 @@ +"use server"; + +export default async function getAllPostAction() { + const response = await fetch(`${process.env.API_URL}/api/post`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + if (!response.ok) { + throw new Error("An error occurred while fetching the post"); + } + return await response.json(); +} + +interface PostData { + title: string; + imageUrl: string; + content: string; + createdBy: string; + createdAt: string; +} + +export async function createPostAction(data: PostData) { + const response = await fetch(`${process.env.API_URL}/api/post`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + if (!response.ok) { + throw new Error("An error occurred while creating the post"); + } + return await response.json(); +} + +export async function getPostByIdAction(id: string) { + const response = await fetch(`${process.env.API_URL}/api/post/${id}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + if (!response.ok) { + throw new Error("An error occurred while fetching the post"); + } + return await response.json(); +} + +export async function deletePostAction(id: string) { + const response = await fetch(`${process.env.API_URL}/api/post/${id}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }); + if (!response.ok) { + throw new Error("An error occurred while deleting the post"); + } + return await response.json(); +} + + +export async function updateVote({ id, vote }: { id: string; vote: number }) { + const response = await fetch(`${process.env.API_URL}/api/post/${id}`, { + method: "PUT", + body: JSON.stringify({ upvote: vote, id }), + headers: { + "Content-Type": "application/json", + }, + }); + console.log("response", response); + if (!response.ok) { + throw new Error("An error occurred while updating the vote"); + } + console.log(response); + return await response.json(); +} + + +export async function addComment(userId: string, { comment, id }: { comment: string, id: string }) { + console.log(id, comment) + const response = await fetch(`${process.env.API_URL}/api/post/${id}/commentPost`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ comment, userId }), + }); + console.log("response", response); + if (!response.ok) { + throw new Error("An error occurred while creating the comment"); + } + return await response.json(); + +} \ No newline at end of file diff --git a/frontend/src/app/context/getUserContext.tsx b/frontend/src/app/context/getUserContext.tsx index 0269592..001cc10 100644 --- a/frontend/src/app/context/getUserContext.tsx +++ b/frontend/src/app/context/getUserContext.tsx @@ -54,7 +54,6 @@ export const UserContextProvider: React.FC = ({ `${process.env.NEXT_PUBLIC_API_URL}/api/users/${user.username}/friends`, { method: "GET", - cache: "force-cache", headers: { "Content-Type": "application/json", diff --git a/frontend/src/app/onboarding/actions.ts b/frontend/src/app/onboarding/actions.ts index fadf9ea..d3b949e 100644 --- a/frontend/src/app/onboarding/actions.ts +++ b/frontend/src/app/onboarding/actions.ts @@ -42,7 +42,6 @@ async function createUserData(formData: { userId: string, petpreference: any, fa method: "POST", headers: { "Content-Type": "application/json", - 'Cache-Control': 'max-age=3600', }, body: JSON.stringify(formData), }); @@ -58,7 +57,6 @@ async function fetchAIOnboardingResult( method: "POST", headers: { "Content-Type": "application/json", - 'Cache-Control': 'max-age=3600', }, body: JSON.stringify({ message: JSON.stringify(formData) }), } diff --git a/frontend/src/app/posts/[id]/page.tsx b/frontend/src/app/posts/[id]/page.tsx new file mode 100644 index 0000000..1943440 --- /dev/null +++ b/frontend/src/app/posts/[id]/page.tsx @@ -0,0 +1,139 @@ +'use client' +import { FormEvent, useContext, useEffect, useState } from "react"; +import Image from "next/image"; +import { redirect, useParams, useRouter } from "next/navigation"; +import { addComment, deletePostAction, getPostByIdAction, updateVote } from "@/app/actions/post-action"; +import { useUser } from "@clerk/nextjs"; + +interface PostData { + id: string; + title: string; + content: string; + createdBy: string; + image_url: string; + comments: { comment: string, userId: string }[]; + upvote: number; + createdAt: string; +} + +export default function Post() { + const { id } = useParams(); + const { user } = useUser(); + const [data, setData] = useState({ + id: "", + title: "", + createdBy: "", + comments: [], + content: "", + image_url: "", + upvote: 0, + createdAt: "", + }); + const [comments, setComments] = useState<{ comment: string, userId: string }[]>([]); + const [comment, setComment] = useState(""); + const router = useRouter(); + const navigate = useRouter() + const [votes, setVotes] = useState(0); + + useEffect(() => { + async function fetchPost() { + try { + const resolve = await getPostByIdAction(`${id}`) as PostData; + if(!resolve) { + return router.replace("/posts"); + } + setData(resolve); + setVotes(resolve.upvote); + console.log(resolve) + setComments(resolve.comments); + console.log(resolve) + } catch (error) { + redirect("/posts"); + console.log(error) + } + } + fetchPost(); + }, [id]); + if(!user) { + return
loading...
+ } + const date = new Date(data.createdAt); + console.log(comments) + const handleUpvote = async () => { + setVotes((prevVotes) => prevVotes + 1); + await updateVote({ id: id as string, vote: votes + 1 }); + }; + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + setComments((prevComments) => [...prevComments, { comment: comment, userId: user.id }]); + await addComment(user.id, { id: id as string, comment: comment }); + setComment(""); + }; + + return ( +
+ + Back + +
+
+ Posted on{" "} + {`${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`} +
+

{data.title}

+

{data.content || "No content"}

+ {data.image_url && ( + Portrait of Founding Fathers + )} +
+ +
+
+ {comments.map((c, index) => ( +
+ {c.userId}: {c.comment} +
+ ))} +
+
handleSubmit(e)} className="mt-4"> + setComment(event.target.value)} + className="w-full p-2 border border-gray-300 rounded-md" + /> +
+ +
+
+ ); +} diff --git a/frontend/src/app/posts/create/page.tsx b/frontend/src/app/posts/create/page.tsx new file mode 100644 index 0000000..4db189d --- /dev/null +++ b/frontend/src/app/posts/create/page.tsx @@ -0,0 +1,103 @@ +'use client'; +import { createPostAction } from '@/app/actions/post-action'; +import { useUser } from '@clerk/nextjs'; +import { useRouter } from 'next/navigation'; +import { ChangeEvent, FormEvent, useState } from 'react'; + +export default function Create() { + const navigate = useRouter(); + const { user } = useUser(); + const [image, setImage] = useState(null); + + if (!user) { + navigate.replace("/"); + return null; + } + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + + const form = event.target as HTMLFormElement; + const title = (form.elements.namedItem('title') as HTMLInputElement).value; + const content = (form.elements.namedItem('content') as HTMLTextAreaElement).value; + const imageUrl = (form.elements.namedItem('imageUrl') as HTMLInputElement).value; + + const post = { + title, + createdBy: user.id, + content, + imageUrl + }; + + // Save post to the database + await createPostAction({ + title: post.title, + content: post.content, + imageUrl: post.imageUrl, + createdBy: post.createdBy, + createdAt: new Date().toISOString(), + }); + + // Navigate back to the home page + navigate.replace("/posts"); + }; + + return ( +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+ + + + +
+
+
+ ); +} diff --git a/frontend/src/app/posts/page.tsx b/frontend/src/app/posts/page.tsx new file mode 100644 index 0000000..55c3032 --- /dev/null +++ b/frontend/src/app/posts/page.tsx @@ -0,0 +1,127 @@ +"use client"; +import { useEffect, useState } from "react"; +import getAllPostAction from "../actions/post-action"; +import { redirect, useRouter } from "next/navigation"; + +interface Post { + _id: string; + title: string; + content: string; + image_url: string; + upvote: number; + createdAt: string; +} + +export default function Posts() { + const [posts, setPosts] = useState([]); + const [search, setSearch] = useState(""); + const navigate = useRouter(); + useEffect(() => { + const getPosts = async () => { + const response = await getAllPostAction() + setPosts(response) + }; + getPosts(); + }, []); + + async function handleForm(event: React.FormEvent) { + event.preventDefault(); + console.log("searching for", search); + if (!search) { + setPosts([]); + return; + } + const response = posts.find((post) => post.title.includes(search)); + setPosts(response ? [response] : []); + } + + function handleOrderByTime() { + setPosts((prevPosts) => + [...prevPosts].sort( + (a, b) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ) + ); + } + + function handleOrderByUpvotes() { + setPosts((prevPosts) => [...prevPosts].sort((a, b) => b.upvote - a.upvote)); + } + + return ( +
+ {/* Search Bar */} +
+
+ setSearch(event.target.value)} + className="p-2 rounded w-full m-4 border-none bg-gray-100" + /> +
+ +
+ + {/* Order By Section */} +
+ Order by: + + +
+ + {/* Posts List */} + +
+ ); +} diff --git a/frontend/src/app/ui/chat/FriendList.tsx b/frontend/src/app/ui/chat/FriendList.tsx index 725d530..83d5625 100644 --- a/frontend/src/app/ui/chat/FriendList.tsx +++ b/frontend/src/app/ui/chat/FriendList.tsx @@ -8,13 +8,13 @@ import { redirect, useRouter } from "next/navigation"; export default function FriendList({ friends, active, me, rooms }: { rooms: Room[], me: User, active: boolean, friends: User[] }) { const router = useRouter(); + console.log(friends) const handleClick = (user: User) => { const isValid = rooms.find((room) => room.recipients.some((r) => r.username === user.username)); if(isValid) return router.push(`/chat/${isValid.roomId}`); const createRoom = async () => { const fetchRoom = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/rooms`, { method: "POST", - cache: "force-cache", headers: { "Content-Type": "application/json", }, diff --git a/frontend/src/app/ui/dashboard/Hero.tsx b/frontend/src/app/ui/dashboard/Hero.tsx index 6046b29..64121e5 100644 --- a/frontend/src/app/ui/dashboard/Hero.tsx +++ b/frontend/src/app/ui/dashboard/Hero.tsx @@ -60,7 +60,6 @@ export default function Hero() { Welcome back, {user.firstName}!

-
diff --git a/frontend/src/app/ui/navigation/Sidebar.tsx b/frontend/src/app/ui/navigation/Sidebar.tsx index 326e5da..83602cd 100644 --- a/frontend/src/app/ui/navigation/Sidebar.tsx +++ b/frontend/src/app/ui/navigation/Sidebar.tsx @@ -15,6 +15,11 @@ const navigation = [ name: "Favorites", href: "/dashboard/favorites", }, + { + name: "posts", + icon: "/dashboard/post.svg", + href: "/posts", + }, { icon: "/dashboard/chat.svg", name: "Chat",