diff --git a/@types/Post.ts b/@types/Post.ts new file mode 100644 index 0000000..1d67eb5 --- /dev/null +++ b/@types/Post.ts @@ -0,0 +1,32 @@ +type Post = { + content?: string | null; + name: string; + username: string; + authorID: string; + category?: + | "art" + | "music" + | "hybrid" + | "theatre" + | "literature" + | "science" + | "sports" + | "other"; + tags?: string[]; + image?: string; + featured?: boolean; + likes?: string[]; + comments?: { + userId: string; + content: string; + username: string; + timestamp: string; + userImg: string; + url: string; + name: string; + }[]; + userImg?: string; + url?: string | null; + createdAt?: string; + updatedAt?: string; +}; diff --git a/app/[username]/page.tsx b/app/[username]/page.tsx index f92573e..8f26f9c 100644 --- a/app/[username]/page.tsx +++ b/app/[username]/page.tsx @@ -1,8 +1,9 @@ -import Navbar from "@/components/Navbar"; import Sidebar from "@/components/Sidebar"; import Widgets from "@/components/Widgets"; import UserProfileData from "@/components/UserProfileData"; import CommentModal from "@/components/CommentModal"; +import PostModal from "@/components/PostModal"; +import { backendUrl } from "../utils/config/backendUrl"; export default async function UserProfile({}) { const { trendingPosts, randomUsersResults } = await getWidgetsData(); @@ -23,6 +24,7 @@ export default async function UserProfile({}) { trendingPosts={trendingPosts || []} randomUsersResults={randomUsersResults?.results || []} /> + ); @@ -32,7 +34,7 @@ export default async function UserProfile({}) { async function getWidgetsData() { - if (!process.env.NEXT_PUBLIC_BACKEND_URL) { + if (!backendUrl) { return { trendingPosts: [], randomUsersResults: [], @@ -40,7 +42,7 @@ async function getWidgetsData() { } const trendingPosts = await fetch( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/widgets/trending/posts` + `${backendUrl}/widgets/trending/posts` ).then((res) => res.json()); // Who to follow section diff --git a/app/api/posts/[id]/comment/route.ts b/app/api/posts/[id]/comment/route.ts index b66475c..d318b3b 100644 --- a/app/api/posts/[id]/comment/route.ts +++ b/app/api/posts/[id]/comment/route.ts @@ -7,32 +7,32 @@ import { NextResponse } from "next/server"; // Private route handle post update export async function PUT(request: Request) { const bearer = request.headers.get("Authorization"); + const userId = request.headers.get("userId"); if (!bearer) { return NextResponse.json({ message: "Not authorized" }, { status: 401 }); } - - const token = bearer.split(" ")[1]; - const data = await request.json(); const postId = request.url.split("/")[5]; - const userId = data?.userId; await connectMongoDB(); + const post = await Post.findOne({ _id: postId }).lean() as any; if (!post) { return NextResponse.json({ message: "No posts found" }, { status: 404 }); } + const { _id, username, name, userImg, content, timestamp, url } = await request.json(); + const newComment = { - _id: new mongoose.Types.ObjectId(), + _id: _id, userId: userId, - content: data.content, - username: data.username, - timestamp: data.timestamp, - url: data.url, - userImg: data.userImg, - name: data.name, + content: content, + username: username, + timestamp: timestamp, + url: url, + userImg: userImg, + name: name, }; post.comments.push(newComment); @@ -73,7 +73,7 @@ export async function DELETE(request: Request) { await post.updateOne({ comments: newComments }); - return NextResponse.json({ post: post }, { status: 200 }); + return NextResponse.json({ message: "Comment deleted" }, { status: 200 }); } catch (error) { console.error("Error: ", error); return NextResponse.json({ message: "Something went wrong" }, { status: 500 }); diff --git a/app/api/posts/[id]/like/route.ts b/app/api/posts/[id]/like/route.ts index 17ae370..d877f7f 100644 --- a/app/api/posts/[id]/like/route.ts +++ b/app/api/posts/[id]/like/route.ts @@ -6,8 +6,7 @@ import { NextResponse } from "next/server"; // Private route handle post update export async function PUT(request: Request) { const postId = request.url.split("/")[5]; - const data = await request.json(); - const userId = data?.userId; + const userId = request.headers.get("userId"); if (!postId || !userId) { return NextResponse.json({ message: "Invalid data" }, { status: 400 }); diff --git a/app/api/posts/route.ts b/app/api/posts/route.ts index c680013..9639f55 100644 --- a/app/api/posts/route.ts +++ b/app/api/posts/route.ts @@ -1,23 +1,44 @@ import { connectMongoDB } from "@/libs/mongodb"; import Post from "../../../models/Post"; -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; +export async function GET(req: NextRequest) { + try { + await connectMongoDB(); + const type = req.nextUrl.searchParams.get("type") || null; -export async function GET() { - await connectMongoDB(); - const posts = await Post.find({}); // fetch data from the source - return NextResponse.json({ posts }); // respond with JSON + if (type) { + const posts = await Post.find({ category: type }); + return NextResponse.json({ posts }); + } + + const posts = await Post.find({}); // fetch data from the source + return NextResponse.json({ posts }); // respond with JSON + } catch (error) { + console.error("Error fetching posts: ", error); + return NextResponse.json({ message: "Error fetching posts from MongoDB!" }); + } } // Private route, handle post creation export async function POST(req) { try { - const { username, name, userImg, content, authorID, category, url } = await req.json(); + const { _id, username, name, userImg, content, authorID, category, url } = + await req.json(); await connectMongoDB(); - const res = await Post.create({ username, name, userImg, content, authorID, category, url }); + const res = await Post.create({ + _id, + username, + name, + userImg, + content, + authorID, + category, + url, + }); return NextResponse.json({ message: "success", post: res }); - } catch (error) { - console.error("Error creating post: ", error); - return NextResponse.json({ message: "error: ", error }); - } + } catch (error) { + console.error("Error creating post: ", error); + return NextResponse.json({ message: "error: ", error }); + } } diff --git a/app/api/widgets/trending/posts/route.ts b/app/api/widgets/trending/posts/route.ts index 09a80d9..4dfd473 100644 --- a/app/api/widgets/trending/posts/route.ts +++ b/app/api/widgets/trending/posts/route.ts @@ -39,7 +39,7 @@ export async function GET(request: Request) { return NextResponse.json({ trendingPosts }, { status: 200 }); } catch (error) { console.error("Error fetching popular posts:", error); - return NextResponse.json({ error: "Failed to fetch popular posts" }, { status: 500 }); + return new NextResponse("Error!", { status: 500 }); } } diff --git a/app/chat/page.tsx b/app/chat/page.tsx index e42ee18..822042f 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -6,6 +6,7 @@ import Sidebar from "../../components/Sidebar"; import Navbar from "../../components/Navbar"; import { PaperAirplaneIcon } from "@heroicons/react/24/outline"; import { useEffect, useState, useRef } from "react"; +import PostModal from "@/components/PostModal"; export default function Chat({}) { const [chats, setChats] = useState([]); @@ -151,14 +152,18 @@ export default function Chat({}) {
- + - {chats.map((chat : any) => ( + {chats.map((chat: any) => (
+ ); } diff --git a/app/events/page.tsx b/app/events/page.tsx index 86311df..e68c1f1 100644 --- a/app/events/page.tsx +++ b/app/events/page.tsx @@ -6,6 +6,8 @@ import EventsLayout from "@/components/EventsLayout"; import { getServerSession } from "next-auth"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import Login from "../login/page"; +import PostModal from "@/components/PostModal"; +import { backendUrl } from "../utils/config/backendUrl"; export default async function Event({}) { @@ -43,68 +45,70 @@ export default async function Event({}) { + ); -} -async function getEvents(session: any) { + async function getEvents(session: any) { - if (!process.env.NEXT_PUBLIC_BACKEND_URL) { - return { - events: [], - }; - } - - const res = await fetch( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/events`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${session?.user?.accessToken}`, - }, + if (!backendUrl || backendUrl === "undefined") { + return { + events: [], + }; } - ); - const data = await res.json(); - return { - events: data?.events || [], - }; -} - -async function getWidgetsData() { - - if (!process.env.NEXT_PUBLIC_BACKEND_URL) { - return { - trendingPosts: [], - randomUsersResults: [], - }; + try { + const res = await fetch(`${backendUrl}/events`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${session?.user?.accessToken}`, + }, + }); + const data = await res.json(); + + return { + events: data?.events || [], + }; + } catch (error) { + return { + events: [], + }; + } } - const trendingPosts = await fetch( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/widgets/trending/posts` - ).then((res) => res.json()); - - // Who to follow section - - let randomUsersResults : any = []; - - try { - const res = await fetch( - "https://randomuser.me/api?results=10&inc=name,login,picture" - ); + async function getWidgetsData() { + if (!backendUrl || backendUrl === "undefined") { + return { + trendingPosts: [], + randomUsersResults: [], + }; + } - randomUsersResults = await res.json(); - } catch (e) { - randomUsersResults = []; + try { + const trendingPostsRes = await fetch( + `${backendUrl}/widgets/trending/posts` + ); + const randomUsersRes = await fetch( + "https://randomuser.me/api/?results=10&inc=name,login,picture" + ); + + const trendingPosts = await trendingPostsRes.json(); + const randomUsersResults = await randomUsersRes.json(); + + return { + trendingPosts: trendingPosts.trendingPosts, + randomUsersResults: randomUsersResults.results, + }; + } catch (error) { + return { + trendingPosts: [], + randomUsersResults: [], + }; + } } - - return { - trendingPosts: trendingPosts?.trendingPosts, - randomUsersResults, - }; } \ No newline at end of file diff --git a/app/notifications/page.tsx b/app/notifications/page.tsx index 8fd2526..5885376 100644 --- a/app/notifications/page.tsx +++ b/app/notifications/page.tsx @@ -1,11 +1,61 @@ -import CommentModal from "@/components/CommentModal"; import Sidebar from "@/components/Sidebar"; import Widgets from "@/components/Widgets"; -import NotificationsLayout from "@/components/Notifications"; import Navbar from "@/components/Navbar"; +import PostModal from "@/components/PostModal"; +import Image from "next/image"; +import moment from "moment"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { BellIcon } from "@heroicons/react/24/outline"; +import { backendUrl } from "../utils/config/backendUrl"; export default async function Notifications({}) { const { trendingPosts, randomUsersResults } = await getWidgetsData(); + const session = await getServerSession(authOptions); + + // create dummy notifications + const notifications = [ + { + _id: "1", + userImg: + "https://images.pexels.com/photos/17745308/pexels-photo-17745308/free-photo-of-city-street-building-wall.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load", + name: "John Doe", + message: "Tesla stock price is going up", + timestamp: Date.now() - 1000 * 60 * 60 * 24 * 2, + }, + { + _id: "2", + userImg: + "https://images.pexels.com/photos/18147099/pexels-photo-18147099/free-photo-of-fantastische-reflektion-im-herbst.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load", + name: "Bela Lugosi", + message: "liked your post", + timestamp: "2023-09-01T16:00:00.000Z", + }, + { + _id: "3", + userImg: + "https://images.pexels.com/photos/12132145/pexels-photo-12132145.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load", + name: "Ramprakash Chatriwala", + message: "challenged you to an art battle", + timestamp: "2023-09-01T16:00:00.000Z", + }, + { + _id: "4", + userImg: + "https://images.pexels.com/photos/18263099/pexels-photo-18263099/free-photo-of-a-car-in-the-forest-under-a-starry-sky.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load", + name: "Sonty Poplu", + message: "Faceoff is starting in 5 minutes", + timestamp: "2023-09-01T16:00:00.000Z", + }, + { + _id: "5", + userImg: + "https://images.pexels.com/photos/15036478/pexels-photo-15036478/free-photo-of-forest-in-autumn.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load", + name: "Emily hill", + message: "challenged you to an art battle", + timestamp: Date.now(), + }, + ]; return (
@@ -14,47 +64,99 @@ export default async function Notifications({}) {
- + + {session && notifications.length > 0 ? ( +
+
+
+
+
+
+

Notifications

+
+
+

Challenges

+
+
+
+
+ + {notifications?.map((notification) => ( +
+ +

+ + {notification.name} + {" "} + {notification.message} +

+ + · + + + {moment(notification.timestamp).fromNow()} + +
+ ))} +
+
+ ) : ( +
+ +

+ You have no notifications +

+
+ )}
+
); -} -async function getWidgetsData() { + async function getWidgetsData() { - if (!process.env.NEXT_PUBLIC_BACKEND_URL) { - return { - trendingPosts: [], - randomUsersResults: [], - }; - } + if (!backendUrl || backendUrl === "undefined") { + return { + trendingPosts: [], + randomUsersResults: [], + }; + } - const trendingPosts = await fetch( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/widgets/trending/posts` - ).then((res) => res.json()); + try { + const trendingPostsRes = await fetch( + `${backendUrl}/widgets/trending/posts` + ); + const randomUsersRes = await fetch( + "https://randomuser.me/api/?results=10&inc=name,login,picture" + ); - // Who to follow section + const trendingPosts = await trendingPostsRes.json(); + const randomUsersResults = await randomUsersRes.json(); - let randomUsersResults : any = []; - - try { - const res = await fetch( - "https://randomuser.me/api/?results=10&inc=name,login,picture" - ); - - randomUsersResults = await res.json(); - } catch (e) { - randomUsersResults = []; + return { + trendingPosts: trendingPosts.trendingPosts, + randomUsersResults: randomUsersResults.results, + }; + } catch (error) { + return { + trendingPosts: [], + randomUsersResults: [], + }; + } } - - return { - trendingPosts: trendingPosts?.trendingPosts, - randomUsersResults, - }; -} +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 06961fe..e2e3b07 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,10 +1,13 @@ import Sidebar from "@/components/Sidebar"; import Widgets from "@/components/Widgets"; -import RefreshFeed from "@/components/RefreshFeed"; -import { Suspense } from "react"; import Feed from "@/components/Feed"; +import { backendUrl } from "./utils/config/backendUrl"; -export default async function Home() { +export default async function Home({ + searchParams, +}: { + searchParams: { [key: string]: string | string[] | undefined }; +}) { const { trendingPosts, randomUsersResults } = await getWidgetsData(); return ( @@ -18,9 +21,9 @@ export default async function Home() {
{/* Feed */} - }> - - + {/* Widgets */} @@ -34,8 +37,6 @@ export default async function Home() { ); async function getWidgetsData() { - const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL; - if (!backendUrl || backendUrl === "undefined") { return { trendingPosts: [], @@ -65,4 +66,4 @@ export default async function Home() { }; } } -} +} \ No newline at end of file diff --git a/app/posts/[id]/page.tsx b/app/posts/[id]/page.tsx index e7a6f87..82d66c2 100644 --- a/app/posts/[id]/page.tsx +++ b/app/posts/[id]/page.tsx @@ -1,9 +1,8 @@ -import { ArrowLeftIcon } from "@heroicons/react/24/outline"; -import CommentModal from "@/components/CommentModal"; import Sidebar from "@/components/Sidebar"; import Widgets from "@/components/Widgets"; import SinglePost from "@/components/SinglePost"; import Navbar from "@/components/Navbar"; +import { backendUrl } from "@/app/utils/config/backendUrl"; export default async function PostPage({ params }) { const { id } = params; @@ -38,7 +37,7 @@ export default async function PostPage({ params }) { async function getWidgetsData() { - if (!process.env.NEXT_PUBLIC_BACKEND_URL) { + if (!backendUrl || backendUrl === "undefined") { return { trendingPosts: [], randomUsersResults: [], @@ -46,7 +45,7 @@ async function getWidgetsData() { } const trendingPosts = await fetch( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/widgets/trending/posts` + `${backendUrl}/widgets/trending/posts` ).then((res) => res.json()); // Who to follow section diff --git a/app/utils/ObjectId.ts b/app/utils/ObjectId.ts new file mode 100644 index 0000000..7f541ae --- /dev/null +++ b/app/utils/ObjectId.ts @@ -0,0 +1,12 @@ +function ObjectId() { + return ( + hex(Date.now() / 1000) + + " ".repeat(16).replace(/./g, () => hex(Math.random() * 16)) + ); +} + +function hex(value: number) { + return Math.floor(value).toString(16); +} + +export default ObjectId; \ No newline at end of file diff --git a/app/utils/config/backendUrl.ts b/app/utils/config/backendUrl.ts new file mode 100644 index 0000000..5fc7796 --- /dev/null +++ b/app/utils/config/backendUrl.ts @@ -0,0 +1 @@ +export const backendUrl = `${process.env.NEXT_PUBLIC_BACKEND_URL}`; \ No newline at end of file diff --git a/app/utils/postUtils.ts b/app/utils/postUtils.ts new file mode 100644 index 0000000..aaf2e1f --- /dev/null +++ b/app/utils/postUtils.ts @@ -0,0 +1,128 @@ +import { backendUrl } from "./config/backendUrl"; + +// TODO: log errors to Sentry +export const addPost = async (post: Post, accessToken: string | undefined) => { + if (!accessToken) { + return; + } + try { + const res = await fetch(`${backendUrl}/posts`, { + method: "POST", + body: JSON.stringify(post), + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + }); + const data = await res.json(); + if (data?.message === "success" || res?.status === 200) { + return true; + } + } catch (error) { + return false; + } +}; + +export const addComment = async ( + comment: any, + accessToken: string | undefined, + postId: string | undefined +) => { + if (!accessToken) { + return; + } + try { + const res = await fetch(`${backendUrl}/posts/${postId}/comment`, { + method: "PUT", + body: JSON.stringify(comment), + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + }); + const data = await res.json(); + + if (res.status === 200 || data?.message === "Comment added successfully!") { + return true; + } + } catch (error) { + return false; + } + return false; +}; + +export const deletePost = async ( + postId: string | undefined, + accessToken: string | undefined +) => { + if (!accessToken) { + return; + } + try { + const res = await fetch(`${backendUrl}/posts/${postId}`, { + method: "DELETE", + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + const data = await res.json(); + if (data?.message === "Post deleted" || data?.status === 200) { + return true; + } + } catch (error) { + return false; + } + return false; +}; + +export const likePost = async ( + postId: string | undefined, + userId: string | undefined, + accessToken: string | undefined +) => { + if (!accessToken) { + return; + } + try { + const res = await fetch(`${backendUrl}/posts/${postId}/like`, { + method: "PUT", + body: JSON.stringify({ userId }), + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + const data = await res.json(); + if ( + data?.message === "Like updated successfully!" || + data?.status === 200 + ) { + return true; + } + } catch (error) { + return false; + } +}; + +export const deleteComment = async ( + commentId: string | undefined, + accessToken: string | undefined +) => { + if (!accessToken) { + return; + } + try { + const res = await fetch(`${backendUrl}/posts/${commentId}/comment`, { + method: "DELETE", + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + const data = await res.json(); + if (data?.message === "Comment deleted" || res?.status === 200) { + return true; + } + } catch (error) { + return false; + } + return false; +}; \ No newline at end of file diff --git a/components/Comment.tsx b/components/Comment.tsx index 3a5e511..63ceffd 100644 --- a/components/Comment.tsx +++ b/components/Comment.tsx @@ -20,6 +20,8 @@ import axios from "axios"; import Image from "next/image"; import YoutubeEmbed from "./YoutubeEmbed"; import { toastError, toastSuccess } from "./Toast"; +import { backendUrl } from "@/app/utils/config/backendUrl"; +import { deleteComment } from "@/app/utils/postUtils"; export default function Comment({ comment, commentId, originalPostId, updatePosts }) { const [likes, setLikes] = useState([]); @@ -53,7 +55,7 @@ export default function Comment({ comment, commentId, originalPostId, updatePost setHasLiked(!hasLiked); //optimistic update const res = await axios.put( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/posts/${originalPostId}/like`, + `${backendUrl}/posts/${originalPostId}/like`, { userId: userId, }, @@ -74,23 +76,18 @@ export default function Comment({ comment, commentId, originalPostId, updatePost }; - const deleteComment = async () => { + const deleteCommentFunc = async () => { updatePosts("delete", null, commentId); // optimistic update - try { - const res = await axios.delete( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/posts/${commentId}/comment`, - { - headers: { - Authorization: `Bearer ${session?.user?.accessToken}`, - }, - } - ); - toastSuccess("Comment deleted", undefined); - } catch (error) { + const res = await deleteComment(commentId, session?.user?.accessToken); + if (!res) { toastError("Error deleting comment", undefined); + updatePosts("add", comment, commentId); // rollback + } else { + toastSuccess("Comment deleted", undefined); } }; + return (
{/* user image */} @@ -173,7 +170,7 @@ export default function Comment({ comment, commentId, originalPostId, updatePost
{session?.user?.id === comment?.userId && ( )} diff --git a/components/CommentModal.tsx b/components/CommentModal.tsx index 9a992de..c007070 100644 --- a/components/CommentModal.tsx +++ b/components/CommentModal.tsx @@ -15,6 +15,7 @@ import { Modal } from "./modal"; import Spinner from "./Spinner"; import Input from "./Input"; import { toastError } from "./Toast"; +import { backendUrl } from "@/app/utils/config/backendUrl"; export default function CommentModal({type, updatePosts}) { const [open, setOpen] = useRecoilState(modalState); @@ -22,6 +23,7 @@ export default function CommentModal({type, updatePosts}) { const [postId] = useRecoilState(postIdState); + // fetch the post useEffect(() => { async function fetchPost() { @@ -29,7 +31,7 @@ export default function CommentModal({type, updatePosts}) { setPost({}); try { const res = await axios.get( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/posts/${postId}` + `${backendUrl}/posts/${postId}` ); setPost(res.data.post); } catch (error) { diff --git a/components/EventForm.tsx b/components/EventForm.tsx index 4eda6d2..f45a5e9 100644 --- a/components/EventForm.tsx +++ b/components/EventForm.tsx @@ -3,6 +3,7 @@ import { Modal } from "./modal"; import { use, useEffect, useState } from "react"; import { useSession } from "next-auth/react"; import { toastError, toastSuccess } from "./Toast"; +import { backendUrl } from "@/app/utils/config/backendUrl"; export default function EventForm({ closeModal }) { const [eventTitle, setEventTitle] = useState(""); @@ -95,7 +96,7 @@ export default function EventForm({ closeModal }) { }; setLoading(true); const res = await axios.post( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/events/${ + `${backendUrl}/events/${ session?.user?.email?.split("@")[0] }`, newEvent, diff --git a/components/Feed.tsx b/components/Feed.tsx index b913af8..01b030f 100644 --- a/components/Feed.tsx +++ b/components/Feed.tsx @@ -5,22 +5,40 @@ import { Key, useEffect, useState } from "react"; import Input from "./Input"; import Post from "./Post"; import Navbar from "./Navbar"; -import axios from "axios"; import { useSession } from "next-auth/react"; import Spinner from "./Spinner"; import { PencilSquareIcon } from "@heroicons/react/24/outline"; import { Modal } from "./modal"; import CommentModal from "./CommentModal"; -import { toastSuccess, toastError } from "./Toast"; +import { toastError } from "./Toast"; import PostModal from "./PostModal"; +import useSWR from "swr"; +import { backendUrl } from "@/app/utils/config/backendUrl"; + export default function Feed({ type }) { - const [posts, setPosts] = useState([]) as any; - const [loading, setLoading] = useState(true); + // fetcher function for useSWR + const fetcher = (url: RequestInfo | URL) => + fetch(url).then((res) => { + if (!res.ok) { + toastError("Error loading posts", undefined); + } + return res.json(); + }); + + const key = type ? `${backendUrl}/posts?type=${type}` : `${backendUrl}/posts`; + const { data, error, isLoading, mutate } = useSWR(key, fetcher, { + // refreshInterval: 30000, + revalidateOnFocus: true, + revalidateOnReconnect: true, + refreshWhenOffline: true, + }); + const [modalOpen, setModalOpen] = useState(false); const { status, data: session } = useSession(); const [width, setWidth] = useState(undefined); + const handleWindowSizeChange = () => { setWidth(window?.innerWidth); }; @@ -30,7 +48,6 @@ export default function Feed({ type }) { handleWindowSizeChange(); }, []); - // call your useEffect useEffect(() => { window.addEventListener("resize", handleWindowSizeChange); return () => { @@ -49,57 +66,53 @@ export default function Feed({ type }) { } }, [modalOpen, width]); - useEffect(() => { - async function getPosts() { - try { - setLoading(true); - const res = await axios.get( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/posts` - ); - if (!type) { - setPosts(res.data.posts); - setLoading(false); - return; - } - const filtered = res.data.posts.filter( - (post: { category: any }) => post.category === type - ); - setPosts(filtered); - setLoading(false); - } catch (error) { - setLoading(false); - toastError("Error loading posts", undefined); - } - } - getPosts(); - }, [type]); + if (error) { + console.error(error); + toastError("Error loading posts", undefined); + } - const updatePosts = (operation: string, newPost: any, id: any) => { + function updatePosts(operation: string, newPost: any, id: any) { + // an update function to update the feed optimistically if (operation === "delete") { - setPosts(posts.filter((post: { _id: any }) => post._id !== id)); - return; + mutate( + (data) => ({ + posts: data.posts.filter((post: { _id: any }) => post._id !== id), + }), + { + revalidate: false, + rollbackOnError: true, + } + ); } - if (operation === "update") { - setPosts([newPost, ...posts]); - return; + if (operation === "update" || operation === "add") { + mutate( + { + posts: [newPost, ...data?.posts], + } + , { + revalidate: false, + rollbackOnError: true, + } + ); } - // if a reply is added to a post if (operation === "reply") { - // find the post and add the reply to it - const post = posts.find((post: { _id: any }) => post._id === id); - // check if the post already has a comment with the same id - const comment = post.comments.find( - (comment: { _id: any }) => comment._id === newPost._id + mutate({ + posts: data?.posts.map((post: any) => { + if (post._id === id) { + post.comments?.push(newPost); + } + return post; + }), + }, + { + revalidate: false, + rollbackOnError: true, + } ); - // if the comment doesn't exist, add it - if (!comment) { - post.comments.push(newPost); - } - return; } }; - if (!loading && posts?.length === 0) { + if (!isLoading && data?.posts?.length === 0) { return (
@@ -121,11 +134,6 @@ export default function Feed({ type }) { ); } - if (!loading || status !== "loading") { - // set overFlow to visible - document.body.style.overflow = "visible"; - } - return ( <>
@@ -155,7 +163,7 @@ export default function Feed({ type }) {
)} - {loading && status !== "loading" ? ( + {isLoading && status !== "loading" ? (
@@ -168,34 +176,34 @@ export default function Feed({ type }) { )} - {posts - ?.slice(0, 20) - .sort( - ( - a: { createdAt: string | number | Date }, - b: { createdAt: string | number | Date } - ) => { - const dateA = new Date(a.createdAt) as any; - const dateB = new Date(b.createdAt) as any; - return dateB - dateA; // Sort in descending order - } - ) - .map((post: { _id: Key | null | undefined }) => ( - - - - ))} + {data?.posts + ?.slice(0, 40) + .sort( + ( + a: { createdAt: string | number | Date }, + b: { createdAt: string | number | Date } + ) => { + const dateA = new Date(a.createdAt) as any; + const dateB = new Date(b.createdAt) as any; + return dateB - dateA; // Sort in descending order + } + ) + .map((post: { _id: Key | null | undefined }) => ( + + + + ))} {modalOpen && ( diff --git a/components/Input.tsx b/components/Input.tsx index 9c5c984..af5a1b4 100644 --- a/components/Input.tsx +++ b/components/Input.tsx @@ -11,12 +11,13 @@ import { useState, useRef, useEffect } from "react"; import axios from "axios"; import Image from "next/image"; import { useSession } from "next-auth/react"; -import { useRouter } from "next/navigation"; import YoutubeEmbed from "./YoutubeEmbed"; import { Modal } from "./modal"; import Spinner from "./Spinner"; -import { toastError, toastLoading, toastSuccess } from "./Toast"; +import { toastError, toastSuccess } from "./Toast"; import toast from "react-hot-toast"; +import ObjectId from "@/app/utils/ObjectId"; +import { addComment, addPost } from "@/app/utils/postUtils"; export default function Input({ text, @@ -26,7 +27,6 @@ export default function Input({ phoneInputModal, setCommentModalState, }) { - const router = useRouter(); const [input, setInput] = useState(""); const [currentUser, setCurrentUser] = useState(null); const [selectedFile, setSelectedFile] = useState(null); @@ -103,66 +103,13 @@ export default function Input({ return str?.replace(//g, ">").slice(0, 300); }; - async function sendReply(e: { preventDefault: () => void }) { - e.preventDefault(); - - if ( - !input.trim() && - !mediaUrl?.trim() && - selectedFile === null && - selectedFile instanceof File === false - ) { - alert("Please enter some text!"); - return; - } - const sanitizedInput = sanitize(input?.trim()); - const sanitizedUrl = sanitize(mediaUrl?.trim()); - setLoading(true); - const reply = { - userId: session?.user?.id, - content: sanitizedInput || "", - username: session?.user?.email.split("@")[0], - timestamp: Date.now(), - userImg: session?.user?.image, - name: session?.user?.name, - category: "other", - url: sanitizedUrl || "", - }; - try { - const res = await axios.put( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/posts/${id}/comment`, - reply, - { - headers: { - Authorization: `Bearer ${session?.user?.accessToken}`, - }, - } - ); - toastSuccess("Reply posted!", undefined); - - if (res?.data?.post && updatePosts && !setCommentModalState) { - setLoading(false); - updatePosts("reply", res.data.post, id); - } - - if (res?.data?.post && updatePosts && setCommentModalState) { - setLoading(false); - updatePosts("reply", res.data.post, id); - setCommentModalState(false); - } - } catch (error) { - toastError("Error posting reply", undefined); - setLoading(false); - } - setLoading(false); - setInput(""); - setMediaUrl(""); - } - function addYoutubeVideo(url: string) { const videoId = url.split("v=")[1]; // if the url is not a valid youtube url - const validFormats = ["https://www.youtube.com/watch?v=", "https://youtu.be/watch?v="]; + const validFormats = [ + "https://www.youtube.com/watch?v=", + "https://youtu.be/watch?v=", + ]; if (url?.length === 0 || !validFormats.includes(url.split(videoId)[0])) { setYoutubeVideoUrl(""); alert("Please enter a valid youtube url!"); @@ -203,6 +150,7 @@ export default function Input({ } if (file.size > 4 * 1024 * 1024) { + setSelectedFile(null); alert("File size should be less than 4mb!"); return; } @@ -234,8 +182,68 @@ export default function Input({ } }; + async function sendReply(e: { preventDefault: () => void }) { + e.preventDefault(); + setLoading(true); + if ( + !input.trim() && + !mediaUrl?.trim() && + selectedFile === null && + selectedFile instanceof File === false + ) { + alert("Please enter some text!"); + return; + } + const sanitizedInput = sanitize(input?.trim()); + const sanitizedUrl = sanitize(mediaUrl?.trim()); + + const reply = { + _id: ObjectId(), + userId: session?.user?.id, + content: sanitizedInput || "", + username: session?.user?.email.split("@")[0], + timestamp: Date.now(), + userImg: session?.user?.image, + name: session?.user?.name, + category: "other", + url: sanitizedUrl || "", + }; + + // early return if the post is empty + if (reply.content?.trim().length === 0 && reply.url?.trim().length === 0) { + setLoading(false); + return; + } + + // if updatePosts is not null, then the post was sent from the home page + if (updatePosts) { + updatePosts("reply", reply, id); + } + + const res = await addComment(reply, session?.user?.accessToken, id); + if (res) { + toastSuccess("Comment added!", undefined); + } else { + toastError("Error adding reply!", undefined); + } + setLoading(false); + + if (phoneInputModal) { + phoneInputModal(false); + } + + if (setCommentModalState) { + setCommentModalState(false); + } + + setLoading(false); + setInput(""); + setMediaUrl(""); + } + async function sendPost(e: { preventDefault: () => void }) { e.preventDefault(); + setLoading(true); if ( !input.trim() && !mediaUrl?.trim() && @@ -250,6 +258,7 @@ export default function Input({ const sanitizedUrl = sanitize(mediaUrl?.trim()); const post = { + _id: ObjectId(), username: session?.user?.email.split("@")[0] || "test", userImg: session?.user?.image || "", content: sanitizedInput || "", @@ -257,41 +266,38 @@ export default function Input({ url: sanitizedUrl || "", name: session?.user?.name || "Test User", authorID: session?.user?.id, - }; + } as Post; + + // early return if the post is empty + if (post.content?.trim().length === 0 && post.url?.trim().length === 0) { + setLoading(false); + return; + } - if (post.content.trim().length > 0 || post.url.trim().length > 0) { - try { - setLoading(true); - - const res = await axios.post( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/posts`, - post, - { - headers: { - Authorization: `Bearer ${session?.user?.accessToken}`, - }, - } - ); - if (phoneInputModal) { - phoneInputModal(); - } - toastSuccess("Post sent!", undefined); - setLoading(false); - if (res?.data?.post && updatePosts && !setCommentModalState) { - updatePosts("update", res.data.post); - } - if (res?.data?.post && updatePosts && setCommentModalState) { - updatePosts("update", res.data.post); - setCommentModalState(false); - } - setInput(""); - setMediaUrl(""); - setSelectedFile(null); - } catch (error) { - setLoading(false); - toastError("Error sending post", undefined); - } + // if updatePosts is not null, then the post was sent from the home page + if (updatePosts) { + updatePosts("add", post, undefined); } + + const res = await addPost(post, session?.user?.accessToken); + if (res) { + toastSuccess("Post sent!", undefined); + } else { + toastError("Error sending post", undefined); + } + setLoading(false); + + if (phoneInputModal) { + phoneInputModal(false); + } + + if (setCommentModalState) { + setCommentModalState(false); + } + + setInput(""); + setMediaUrl(""); + setSelectedFile(null); } function handleInput(e: { target: { innerText: string } }) { @@ -321,6 +327,7 @@ export default function Input({
{currentUser && (
{ const { data } = await axios.get( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/events`, + `${backendUrl}/events`, // add accessToken to headers { headers: { diff --git a/components/MapModal.tsx b/components/MapModal.tsx index db45e3d..dee0f95 100644 --- a/components/MapModal.tsx +++ b/components/MapModal.tsx @@ -6,6 +6,7 @@ import { useMemo, useState } from "react"; import axios from "axios"; import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/outline"; import { toastError, toastSuccess } from "./Toast"; +import { backendUrl } from "@/app/utils/config/backendUrl"; export default function MapModal({ event, closeModal, user}) { const [liveBtnClicked, setLiveBtnClicked] = useState(false); @@ -30,7 +31,7 @@ export default function MapModal({ event, closeModal, user}) { if (!secureToken.trim()) return; try { const res = await axios.post( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/events/live`, + `${backendUrl}/events/live`, { userId: user?.id, eventId: event?._id, diff --git a/components/Notifications.tsx b/components/Notifications.tsx deleted file mode 100644 index 04beacb..0000000 --- a/components/Notifications.tsx +++ /dev/null @@ -1,135 +0,0 @@ -"use client"; - -import { useSession } from "next-auth/react"; -import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; -import Moment from "react-moment"; -import { useRecoilState } from "recoil"; -import { ArrowLeftIcon, BellIcon } from "@heroicons/react/24/outline"; -import Navbar from "./Navbar"; -import Image from "next/image"; -import Spinner from "./Spinner"; - -export default function NotificationsLayout({}) { - const { status, data: session } = useSession(); - const router = useRouter(); - - const [currentUser, setCurrentUser] = useState(null) as any; - - // create dummy notifications - const dummyData = [ - { - _id: "1", - userImg: - "https://images.pexels.com/photos/17745308/pexels-photo-17745308/free-photo-of-city-street-building-wall.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load", - name: "John Doe", - message: "Tesla stock price is going up", - timestamp: Date.now() - 1000 * 60 * 60 * 24 * 2, - }, - { - _id: "2", - userImg: - "https://images.pexels.com/photos/18147099/pexels-photo-18147099/free-photo-of-fantastische-reflektion-im-herbst.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load", - name: "Bela Lugosi", - message: "liked your post", - timestamp: "2023-09-01T16:00:00.000Z", - }, - { - _id: "3", - userImg: - "https://images.pexels.com/photos/12132145/pexels-photo-12132145.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load", - name: "Ramprakash Chatriwala", - message: "challenged you to an art battle", - timestamp: "2023-09-01T16:00:00.000Z", - }, - { - _id: "4", - userImg: - "https://images.pexels.com/photos/18263099/pexels-photo-18263099/free-photo-of-a-car-in-the-forest-under-a-starry-sky.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load", - name: "Sonty Poplu", - message: "Faceoff is starting in 5 minutes", - timestamp: "2023-09-01T16:00:00.000Z", - }, - { - _id: "5", - userImg: - "https://images.pexels.com/photos/15036478/pexels-photo-15036478/free-photo-of-forest-in-autumn.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load", - name: "Emily hill", - message: "challenged you to an art battle", - timestamp: Date.now(), - }, - ]; - - const [notifications, setNotifications] = useState(dummyData); - - useEffect(() => { - if (session) { - setCurrentUser(session?.user); - } else { - setCurrentUser(null); - } - }, [session]); - - if (status === "loading") { - return ( -
- -
- ); - } - - return ( -
- {status === "unauthenticated" && notifications?.length === 0 ? ( -
- -

- No notifications -

-

- You will see notifications about your account here -

-
- ) : ( -
-
-
-
-
-

Notifications

-
-
-

Challenges

-
-
-
-
- - {notifications?.map((notification) => ( -
- -

- {notification.name}{" "} - {notification.message} -

- · - - {notification.timestamp} - -
- ))} -
- )} -
- ); - -} diff --git a/components/Post.tsx b/components/Post.tsx index 5c362ed..477defa 100644 --- a/components/Post.tsx +++ b/components/Post.tsx @@ -20,7 +20,7 @@ import { useRecoilState } from "recoil"; import { modalState, postIdState } from "@/app/atom/modalAtom"; import YoutubeEmbed from "./YoutubeEmbed"; import { toastError, toastSuccess } from "./Toast"; - +import { deletePost, likePost } from "@/app/utils/postUtils"; export default function Post({ post, id, updatePosts }) { const [hasLiked, setHasLiked] = useState(false); @@ -41,7 +41,6 @@ export default function Post({ post, id, updatePosts }) { } }, [status, session]); - // check if post's likes array contains the logged in user's id useEffect(() => { // set the likes state @@ -50,7 +49,7 @@ export default function Post({ post, id, updatePosts }) { } }, [post, session?.user?.id]); - async function likePost() { + async function likePostFunc() { if (currentUser) { setHasLiked(!hasLiked); //optimistic update if (!hasLiked) { @@ -62,23 +61,16 @@ export default function Post({ post, id, updatePosts }) { post?.likes?.splice(index, 1); } } - const config = { - headers: { - Authorization: `Bearer ${session?.user?.accessToken}`, - }, - }; - const res = await axios.put( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/posts/${id}/like`, - { - userId: session?.user?.id, - token: session?.user?.accessToken, - }, - config - ); + const res = await likePost(id, session?.user?.id ,session?.user?.accessToken); //rollback optimistic update if error - if (res.status !== 200) { + if (!res) { + toastError("Error liking post", undefined); setHasLiked(hasLiked); + const index = post?.likes?.indexOf(session?.user?.id); + if (index > -1) { + post?.likes?.splice(index, 1); + } } } else { // signIn(); @@ -86,30 +78,26 @@ export default function Post({ post, id, updatePosts }) { } } - async function deletePost() { + async function deletePostFunc() { if (window.confirm("Are you sure you want to delete this post?")) { updatePosts("delete", null, id); // optimistic update - try { - const res = await axios.delete( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/posts/${id}`, - { - headers: { - Authorization: `Bearer ${session?.user?.accessToken}`, - }, - } - ); - toastSuccess("Post deleted", undefined); - } catch (error) { + const res = await deletePost(id, session?.user?.accessToken); + + //rollback optimistic update if error + if (!res) { toastError("Error deleting post", undefined); + updatePosts("add", post, id); + } else { + toastSuccess("Post deleted", undefined); } } } return (
{/* user image */} router.push(`/posts/${id}`)}> +

router.push(`/posts/${id}`)} + className="text-gray-800 dark:text-darkText whitespace-normal text-[15px] sm:mr-2 sm:text-[16px] mb-2" + > + {post?.content} +

-

router.push(`/posts/${id}`)} - className="text-gray-800 dark:text-darkText whitespace-normal text-[15px] sm:mr-2 sm:text-[16px] mb-2" - > - {post?.content} -

- - {/* post media */} - - { - // check if post?.url exists - post?.url && !post?.url?.includes("youtube") && ( - router.push(`/posts/${id}`)} - className="rounded-2xl max-h-80 w-[100%] sm:w-full object-cover" - width={500} - height={500} - src={post?.url} - alt="img" - /> - ) - } + {/* post media */} - {post?.url && post?.url?.includes("youtube") && ( - - )} + { + // check if post?.url exists + post?.url && !post?.url?.includes("youtube") && ( + router.push(`/posts/${id}`)} + className="rounded-2xl max-h-80 w-[100%] sm:w-full object-cover" + width={500} + height={500} + src={post?.url} + alt="img" + /> + ) + } + {post?.url && post?.url?.includes("youtube") && ( + + )}
{/* icons */} @@ -208,19 +194,20 @@ export default function Post({ post, id, updatePosts }) {
{userId === post?.authorID && ( )}
{hasLiked ? ( ) : ( )} diff --git a/components/PostModal.tsx b/components/PostModal.tsx index dc5cb9a..5e2c61b 100644 --- a/components/PostModal.tsx +++ b/components/PostModal.tsx @@ -13,9 +13,11 @@ export default function PostModal({ type, updatePosts }) {
{open && ( setOpen(false)}> -
+
- + {type && "Express yourself"}
diff --git a/components/RefreshFeed.tsx b/components/RefreshFeed.tsx deleted file mode 100644 index d30cc35..0000000 --- a/components/RefreshFeed.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; -import Feed from "@/components/Feed"; -import { useSearchParams } from "next/navigation"; -export default function RefreshFeed() { - - const searchParams = useSearchParams(); - - const path = searchParams.get("feed"); - - const handleType = (type: string) => { - switch (type) { - case "music": - return ; - case "art": - return ; - case "film": - return ; - case "lit": - return ; - } - } - return ( -
- { path? - handleType(path) : - } -
- ); -} \ No newline at end of file diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index ba7557e..98afe26 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -23,7 +23,7 @@ export default function Sidebar({}) { }, [session?.user, status]); return ( -
+
router.push("/")} className="hoverEffect flex justify-center content-center align-middle p-2 hover:bg-orange-100 xl:px-2" @@ -55,15 +55,20 @@ export default function Sidebar({}) { {currentUser ? ( <> -
+
user-img diff --git a/components/SidebarMenu.tsx b/components/SidebarMenu.tsx index bd05945..d6174b2 100644 --- a/components/SidebarMenu.tsx +++ b/components/SidebarMenu.tsx @@ -169,7 +169,9 @@ export default function SidebarMenu({username, nav, toggleNavbar}) { {modalOpen && ( -
+

Settings

@@ -183,6 +185,7 @@ export default function SidebarMenu({username, nav, toggleNavbar}) {