diff --git a/app/(Main)/[username]/[url]/layout.tsx b/app/(Main)/[username]/[url]/layout.tsx index 8e84046..609b574 100644 --- a/app/(Main)/[username]/[url]/layout.tsx +++ b/app/(Main)/[username]/[url]/layout.tsx @@ -107,7 +107,7 @@ export default async function PostLayout( published: true, }, include: { - _count: { select: { comments: true, savedUsers: true, likes: true } }, + _count: { select: { comments: true, savedUsers: true, likes: true, shares: true } }, author: { include: { Followers: true, @@ -181,7 +181,7 @@ export default async function PostLayout( published: true, }, include: { - _count: { select: { comments: true, savedUsers: true, likes: true } }, + _count: { select: { comments: true, savedUsers: true, likes: true, shares: true } }, author: { include: { Followers: true, diff --git a/app/(Main)/[username]/loading.tsx b/app/(Main)/[username]/loading.tsx new file mode 100644 index 0000000..b872516 --- /dev/null +++ b/app/(Main)/[username]/loading.tsx @@ -0,0 +1,62 @@ + +import PostCardSkeletonV2 from "@/components/skeletons/post-card-2"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function Loading() { + return ( + <> +
+
+
+
+
+
+
+ +
+
+ + +
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ +
    + + + +
+
+
+
+
+
+ + + + + +
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/(Main)/explore/page.tsx b/app/(Main)/explore/page.tsx index 19d3a1d..d15ca41 100644 --- a/app/(Main)/explore/page.tsx +++ b/app/(Main)/explore/page.tsx @@ -1,22 +1,9 @@ -import { EmptyPlaceholder } from "@/components/empty-placeholder" import Posts from "@/components/explore/posts" import Search from "@/components/explore/search" import { getSessionUser } from "@/components/get-session-user" -import { Icons } from "@/components/icon" -import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" -import { Input } from "@/components/ui/input" import { getPosts } from "@/lib/prisma/posts" -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" -import Link from "next/link" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { redirect } from "next/navigation" -import { Hash } from "lucide-react" import { getUsers } from "@/lib/prisma/users" -import { Separator } from "@/components/ui/separator" -import { Button } from "@/components/ui/button" -import UserHoverCard from "@/components/user-hover-card" import { searchTags } from "@/lib/prisma/tags" -import { Badge } from "@/components/ui/badge" import ExploreComponent from "@/components/explore/tab-content" import ExploreTab from "@/components/explore/tab" import Users from "@/components/explore/users" diff --git a/app/(Main)/page.tsx b/app/(Main)/page.tsx index 50e4452..17ca0e3 100644 --- a/app/(Main)/page.tsx +++ b/app/(Main)/page.tsx @@ -36,6 +36,15 @@ const [latestPosts, tags, popularPosts] = await Promise.all([ readingTime: true, publishedAt: true, cover: true, + _count: { + select: { + likes: true, + savedUsers: true, + readedUsers: true, + shares: true, + comments: true, + }, + }, author: { select: { username: true, diff --git a/app/(Main)/tags/[tagname]/page.tsx b/app/(Main)/tags/[tagname]/page.tsx index c7aed88..7bbe68e 100644 --- a/app/(Main)/tags/[tagname]/page.tsx +++ b/app/(Main)/tags/[tagname]/page.tsx @@ -4,9 +4,10 @@ import { getSessionUser } from "@/components/get-session-user"; import TagDetails from "@/components/tags/details"; import TagLatestPosts from "@/components/tags/latest-posts"; import TagPopularPosts from "@/components/tags/post"; +import TagFollowers from "@/components/tags/users"; import { Separator } from "@/components/ui/separator"; import postgres from "@/lib/postgres"; -import { revalidatePath } from "next/cache"; +import { getFollowersByTag } from "@/lib/prisma/tags"; import { redirect } from "next/navigation"; export default async function TagPage({ params }: { params: { tagname: string } }) { @@ -38,7 +39,7 @@ export default async function TagPage({ params }: { params: { tagname: string } }, savedUsers: true, likes: true, - _count: { select: { comments: true, likes: true, savedUsers: true } }, + _count: { select: { comments: true, likes: true, savedUsers: true, shares: true } }, }, orderBy: [ { likes: { _count: 'desc' } }, @@ -65,7 +66,7 @@ export default async function TagPage({ params }: { params: { tagname: string } Followings: true, } }, - _count: { select: { comments: true, likes: true, savedUsers: true } }, + _count: { select: { comments: true, likes: true, savedUsers: true, shares: true } }, savedUsers: true, }, orderBy: { @@ -75,6 +76,8 @@ export default async function TagPage({ params }: { params: { tagname: string } }); if (!tag) redirect("/404"); const session = await getSessionUser(); + + const { followers } = await getFollowersByTag({ id: tag.id, limit: 5, session: session?.id }); return ( <>
@@ -85,6 +88,14 @@ export default async function TagPage({ params }: { params: { tagname: string } )} + { + followers.length > 0 && ( + <> + + + + ) + } { latestPosts.length > 0 && ( <> diff --git a/app/api/comments/[id]/like/route.ts b/app/api/comments/[id]/like/route.ts index 77a1a5e..17ef7b8 100644 --- a/app/api/comments/[id]/like/route.ts +++ b/app/api/comments/[id]/like/route.ts @@ -29,14 +29,89 @@ export async function POST(req: Request) { id: isLiked.id, }, }); - console.log("Deleted like"); + + const oneWeekAgo = new Date(); + oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); + if (isLiked.createdAt > oneWeekAgo) { + const notification = await postgres.notification.findFirst({ + where: { + senderId: user.id, + receiverId: comment.authorId, + type: "commentLike", + }, + orderBy: { + createdAt: "desc", + }, + select: { + id: true, + }, + }); + + if (notification) { + await postgres.notification.delete({ + where: { + id: notification.id, + }, + }); + } + } } else { - await postgres.commentLike.create({ + const commentLike = await postgres.commentLike.create({ data: { commentId, authorId: user.id, }, + include: { + comment: { + select: { + post: { + include: { + author: { + select: { + username: true, + }, + }, + } + }, + content: true, + }, + }, + }, + }); + + const sender = await postgres.user.findUnique({ + where: { + id: user.id, + }, + select: { + id: true, + username: true, + }, }); + + const receiver = await postgres.user.findUnique({ + where: { + id: comment.authorId, + }, + select: { + id: true, + }, + }); + + if (sender && receiver) { + const message = `"${commentLike.comment.content}"`; + const type = "commentLike"; + const url = `/@${commentLike.comment.post.author.username}/${commentLike.comment.post.url}?commentsOpen=true`; + await postgres.notification.create({ + data: { + content: message, + type, + url, + receiverId: receiver?.id || "", + senderId: sender?.id || "", + }, + }); + } console.log("Created like"); } return new Response(null, { status: 200 }); diff --git a/app/api/follow/route.ts b/app/api/follow/route.ts index cbc5cd7..55203e5 100644 --- a/app/api/follow/route.ts +++ b/app/api/follow/route.ts @@ -29,6 +29,33 @@ export async function GET(request: NextRequest) { }, }); + // if followed during 1 week, delete notification + const oneWeekAgo = new Date(); + oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); + if (isFollowed.createdAt > oneWeekAgo) { + const notification = await postgres.notification.findFirst({ + where: { + senderId: followerId, + receiverId: followeeId, + type: "follow", + }, + orderBy: { + createdAt: "desc", + }, + select: { + id: true, + }, + }); + + if (notification) { + await postgres.notification.delete({ + where: { + id: notification.id, + }, + }); + } + } + return NextResponse.json({ message: "unfollowed" }, { status: 200 }); } else { await postgres.follow.create({ @@ -40,33 +67,33 @@ export async function GET(request: NextRequest) { // Check if followerId and followeeId are not null if (followerId && followeeId) { - //Create notification - const sender = await postgres.user.findUnique({ - where: { - id: followerId, - }, - }); - - const receiver = await postgres.user.findUnique({ - where: { - id: followeeId, - }, + //Create notification + const sender = await postgres.user.findUnique({ + where: { + id: followerId, + }, + }); + + const receiver = await postgres.user.findUnique({ + where: { + id: followeeId, + }, + }); + + // Check if sender and receiver are not null + if (sender && receiver) { + const message = `${sender?.name || sender?.username} is now following you`; + const type = "follow"; + const url = `/@${sender?.username}`; + await create({ + content: message, + type, + url, + receiverId: receiver?.id || "", + senderId: sender?.id || "", }); - - // Check if sender and receiver are not null - if (sender && receiver) { - const message = `${sender?.name || sender?.username} followed you`; - const type = "follow"; - const url = `/@${sender?.username}` - await create({ - content: message, - type, - url, - receiverId: receiver?.id || "", - senderId: sender?.id || "", - }); - } } + } return NextResponse.json({ message: "followed" }, { status: 200 }); } diff --git a/app/api/post/[postid]/like/route.ts b/app/api/post/[postid]/like/route.ts index 87c9fe0..b5081fe 100644 --- a/app/api/post/[postid]/like/route.ts +++ b/app/api/post/[postid]/like/route.ts @@ -29,6 +29,33 @@ export async function POST(req: Request) { id: isLiked.id, }, }); + + const oneWeekAgo = new Date(); + oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); + if (isLiked.createdAt > oneWeekAgo) { + const notification = await postgres.notification.findFirst({ + where: { + senderId: user.id, + receiverId: post.authorId, + type: "postLike", + }, + orderBy: { + createdAt: "desc", + }, + select: { + id: true, + }, + }); + + if (notification) { + await postgres.notification.delete({ + where: { + id: notification.id, + }, + }); + } + } + console.log("Deleted like"); } else { await postgres.like.create({ @@ -45,6 +72,31 @@ export async function POST(req: Request) { }, }, }); + + const sender = await postgres.user.findUnique({ + where: { + id: user.id, + }, + }); + const receiver = await postgres.user.findUnique({ + where: { + id: post.authorId, + }, + }); + if (sender && receiver) { + const message = `"${post.title}"`; + const type = "postLike"; + + await postgres.notification.create({ + data: { + content: message, + type, + url: `/@${sender.username}/${post.url}`, + receiverId: receiver.id, + senderId: sender.id, + } + }); + } console.log("Created like"); } return new Response(null, { status: 200 }); diff --git a/app/api/user/[id]/bookmarks/route.ts b/app/api/user/[id]/bookmarks/route.ts index 4d92bde..ea13daf 100644 --- a/app/api/user/[id]/bookmarks/route.ts +++ b/app/api/user/[id]/bookmarks/route.ts @@ -14,6 +14,15 @@ export async function GET(req: NextRequest, { params} : { params: { id: string } include: { author: true, savedUsers: true, + _count: { + select: { + likes: true, + savedUsers: true, + readedUsers: true, + shares: true, + comments: true, + }, + }, } }, }, diff --git a/app/api/user/[id]/history/route.ts b/app/api/user/[id]/history/route.ts index f78d1dd..52e1ace 100644 --- a/app/api/user/[id]/history/route.ts +++ b/app/api/user/[id]/history/route.ts @@ -13,8 +13,18 @@ export async function GET(req: NextRequest, { params} : { params: { id: string } post: { include: { author: true, + _count: { + select: { + likes: true, + savedUsers: true, + readedUsers: true, + shares: true, + comments: true, + }, + }, } }, + }, orderBy: { updatedAt: "desc", diff --git a/app/api/user/[id]/posts/route.ts b/app/api/user/[id]/posts/route.ts index ac1997c..3b7991c 100644 --- a/app/api/user/[id]/posts/route.ts +++ b/app/api/user/[id]/posts/route.ts @@ -9,8 +9,11 @@ const baseQuery = { savedUsers: true, _count: { select: { - likes: true, - savedUsers: true, + likes: true, + savedUsers: true, + readedUsers: true, + shares: true, + comments: true, }, }, tags: { diff --git a/app/globals.css b/app/globals.css index dfcfa59..977a705 100644 --- a/app/globals.css +++ b/app/globals.css @@ -569,4 +569,15 @@ .details{ min-height: calc(100vh - 44rem); } -} */ \ No newline at end of file +} */ +.swiper { + width: 100%; + height: 100%; +} + +.swiper-slide img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} \ No newline at end of file diff --git a/components/blog/feed-post-card.tsx b/components/blog/feed-post-card.tsx index 2bf89d7..87fc7cd 100644 --- a/components/blog/feed-post-card.tsx +++ b/components/blog/feed-post-card.tsx @@ -19,6 +19,7 @@ import { usePathname } from "next/navigation"; import { Skeleton } from "../ui/skeleton"; import { shimmer, toBase64 } from "@/lib/image"; import { validate } from "@/lib/revalidate"; +import PostAnalyticsDialog from "./post-analytics-dialog"; export default function FeedPostCard( @@ -92,14 +93,22 @@ export default function FeedPostCard(

{props.post.readingTime}

-
-
-
-
-
-
- @@ -76,27 +77,30 @@ export default function MobilePostTabs({ post: initialPost, className, session,
- { - session ? ( - - ) : ( - - - + ) : ( + + + + ) + } +
+ { + session?.id === post.authorId && ( + <> + + + ) } -
- {/* - - */} diff --git a/components/blog/navbar.tsx b/components/blog/navbar.tsx index cd1a522..07cc0b9 100644 --- a/components/blog/navbar.tsx +++ b/components/blog/navbar.tsx @@ -14,6 +14,7 @@ import { Post } from "@prisma/client"; import { revalidatePath } from "next/cache"; import { validate } from "@/lib/revalidate"; import { Icons } from "../icon"; +import PostAnalyticsDialog from "./post-analytics-dialog"; export default function PostTabs({ post: initialPost, className, session, author, onClicked }: { post: any, className?: string, session: any, author: any, onClicked: () => void }) { const [post, setPost] = useState(initialPost); @@ -25,21 +26,21 @@ export default function PostTabs({ post: initialPost, className, session, author await fetch(`/api/post/${postId}/like`, { method: "POST", headers: { - "Content-Type": "application/json", + "Content-Type": "application/json", }, body: JSON.stringify({ postId }), - }); - await validate(pathname) + }); + await validate(pathname) } const save = async (postId: Post['id']) => { await fetch(`/api/post/${postId}/save`, { method: "POST", headers: { - "Content-Type": "application/json", + "Content-Type": "application/json", }, body: JSON.stringify({ postId }), - }); - await validate(pathname) + }); + await validate(pathname) } const isLiked = post?.likes?.some((like: any) => like.authorId === session?.id); const isSaved = post?.savedUsers?.some((savedUser: any) => savedUser.userId === session?.id); @@ -101,6 +102,14 @@ export default function PostTabs({ post: initialPost, className, session, author Share + { + session?.id === post.authorId && ( + <> + + + + ) + } { session?.id === post?.authorId && ( <> diff --git a/components/blog/post-analytics-dialog.tsx b/components/blog/post-analytics-dialog.tsx new file mode 100644 index 0000000..5a139c3 --- /dev/null +++ b/components/blog/post-analytics-dialog.tsx @@ -0,0 +1,108 @@ +import { Post, User } from "@prisma/client"; +import React from "react"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Button, buttonVariants } from "../ui/button"; +import { Icons } from "../icon"; +import { BarChart2, Bookmark, Facebook, Link2, Linkedin, MoreHorizontal } from "lucide-react"; +import { LinkedInLogoIcon, FaceIcon, Cross2Icon } from "@radix-ui/react-icons"; +import { FacebookShareButton, LinkedinShareButton, TwitterShareButton } from "next-share"; +import { cn } from "@/lib/utils"; +import { toast } from "../ui/use-toast"; +import PostCard from "../tags/post-card-v2"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; +import { BlurImage as Image } from "../image"; +import { shimmer, toBase64 } from "@/lib/image"; +import { formatNumberWithSuffix } from "../format-numbers"; + + +export default function PostAnalyticsDialog({ post, className, ...props }: { post: any, className?: string } & React.ComponentPropsWithoutRef) { + return ( + <> + + + + + + + Post Analytics + +
+ + +
+ + + {post.title} + + + {post.subtitle} + + + {post.cover && (
+
+ + {post.title} + +
+
+ )} +
+
+
+

{post.readingTime}

+
+
+
+
+ + +
+ Views + {formatNumberWithSuffix(post.views)} + +
+
+ Likes + {formatNumberWithSuffix(post._count.likes)} +
+
+ Comments + {formatNumberWithSuffix(post._count.comments)} + +
+
+ Saves + {formatNumberWithSuffix(post._count.savedUsers)} + +
+
+ Shares + {formatNumberWithSuffix(post._count.shares)} + +
+
+
+
+
+
+ + + ) +} \ No newline at end of file diff --git a/components/explore/tab.tsx b/components/explore/tab.tsx index 1dc57cd..c8cd8ec 100644 --- a/components/explore/tab.tsx +++ b/components/explore/tab.tsx @@ -28,7 +28,7 @@ export default function ExploreTab({ activeTab = 'top', search }: { activeTab?: Users - + Tags diff --git a/components/explore/tags.tsx b/components/explore/tags.tsx index da94068..6c6adfd 100644 --- a/components/explore/tags.tsx +++ b/components/explore/tags.tsx @@ -8,7 +8,7 @@ import { cn } from '@/lib/utils'; import { Separator } from '@radix-ui/react-context-menu'; import { Button } from '../ui/button'; import { EmptyPlaceholder } from '../empty-placeholder'; -import UserHorizontalCard from '../user-horizontal-card'; +import UserHorizontalCard from '../user-vertical-card'; import UserHoverCard from '../user-hover-card'; import Link from 'next/link'; import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; diff --git a/components/explore/users.tsx b/components/explore/users.tsx index be621fc..230c410 100644 --- a/components/explore/users.tsx +++ b/components/explore/users.tsx @@ -8,7 +8,7 @@ import { cn } from '@/lib/utils'; import { Separator } from '@radix-ui/react-context-menu'; import { Button } from '../ui/button'; import { EmptyPlaceholder } from '../empty-placeholder'; -import UserHorizontalCard from '../user-horizontal-card'; +import UserHorizontalCard from '../user-vertical-card'; import UserHoverCard from '../user-hover-card'; import Link from 'next/link'; import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; diff --git a/components/feed/navbar/navbar.tsx b/components/feed/navbar/navbar.tsx index e27d3bf..341b86e 100644 --- a/components/feed/navbar/navbar.tsx +++ b/components/feed/navbar/navbar.tsx @@ -28,22 +28,22 @@ export default function FeedTabs({ tabs, activeTab = 'foryou', children }: { tab
-
+
-
{ +
{ router.replace('/feed') }}> For You
-
{ +
{ router.replace('/feed?tab=following') }}> Following
{tabs?.map((item: any, index: number) => ( -
{ +
{ router.replace(`/feed?tab=${item.tag.name}`) }} key={item.tag.id}> diff --git a/components/icon.tsx b/components/icon.tsx index 1406763..4575eae 100644 --- a/components/icon.tsx +++ b/components/icon.tsx @@ -39,6 +39,17 @@ export const Icons = { close: (props: IconProps) => ( + + ), + eye: (props: IconProps) => ( + + + + + ), + paperAirplane: (props: IconProps) => ( + + ), spinner: Loader2, @@ -48,7 +59,11 @@ export const Icons = { - + ), + analytics: (props: IconProps) => ( + + + ), post: (props: IconProps) => ( diff --git a/components/navbar/navbar.tsx b/components/navbar/navbar.tsx index ede0fdc..da575d6 100644 --- a/components/navbar/navbar.tsx +++ b/components/navbar/navbar.tsx @@ -69,4 +69,4 @@ function Navbar(notifications: any) { ); } -export default Navbar; \ No newline at end of file +export default Navbar; diff --git a/components/notifications/list.tsx b/components/notifications/list.tsx index 7443166..bbf4b14 100644 --- a/components/notifications/list.tsx +++ b/components/notifications/list.tsx @@ -38,30 +38,30 @@ export default function NotificationList({ notifications, ...props }: { notifica {notification?.sender?.name || notification?.sender?.username} } {notification?.type === 'comment' && ( <> - commented on your post + commented on your post: {notification.content} )} {notification?.type === 'like' && ( liked your post )} {notification?.type === 'follow' && ( - started following you + is now following you )} { notification.type === 'reply' && ( <> - replied to your comment on + replied to your comment: {notification.content} )} { notification.type === 'commentLike' && ( <> - liked your comment on + liked your comment: {notification.content} )} { notification.type === 'postLike' && ( <> - liked your post + liked your post: {notification.content} ) diff --git a/components/skeletons/post-card-1.tsx b/components/skeletons/post-card-1.tsx index b5dba29..b49fb9d 100644 --- a/components/skeletons/post-card-1.tsx +++ b/components/skeletons/post-card-1.tsx @@ -2,7 +2,7 @@ import { cn } from "@/lib/utils"; import { Card, CardContent } from "../ui/card"; import { Skeleton } from "../ui/skeleton"; -export default function PostCardSkeletonV2( +export default function PostCardSkeleton( { className, ...props }: React.ComponentPropsWithoutRef & { className?: string; } ) { return ( diff --git a/components/skeletons/post-card-2.tsx b/components/skeletons/post-card-2.tsx index 5dc7bf8..c9bb01a 100644 --- a/components/skeletons/post-card-2.tsx +++ b/components/skeletons/post-card-2.tsx @@ -2,30 +2,28 @@ import { cn } from "@/lib/utils"; import { Card, CardContent, CardHeader } from "../ui/card"; import { Skeleton } from "../ui/skeleton"; -export default function PostCardSkeleton( +export default function PostCardSkeletonV2( { className, ...props }: React.ComponentPropsWithoutRef & { className?: string; } ) { return ( -
- - - +
-
+
-
+
@@ -34,7 +32,7 @@ export default function PostCardSkeleton(
- +
@@ -42,7 +40,7 @@ export default function PostCardSkeleton(
-
+
@@ -50,7 +48,7 @@ export default function PostCardSkeleton(
- +
diff --git a/components/tags/post-card-v2.tsx b/components/tags/post-card-v2.tsx index 6dbd9de..7a54ed9 100644 --- a/components/tags/post-card-v2.tsx +++ b/components/tags/post-card-v2.tsx @@ -11,7 +11,7 @@ import { BlurImage as Image } from "../image"; import { Button } from "../ui/button"; import Link from "next/link"; import { cn } from "@/lib/utils"; -import { MoreHorizontal } from "lucide-react"; +import { BarChart2, MoreHorizontal } from "lucide-react"; import { Badge } from "../ui/badge"; import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; import UserHoverCard from "../user-hover-card"; @@ -24,6 +24,7 @@ import LoginDialog from "../login-dialog"; import { Skeleton } from "../ui/skeleton"; import { shimmer, toBase64 } from "@/lib/image"; import { validate } from "@/lib/revalidate"; +import PostAnalyticsDialog from "../blog/post-analytics-dialog"; export default function PostCard( @@ -114,16 +115,25 @@ export default function PostCard(

{props.post.readingTime}

-
+ { + props.post.published && ( + props.session?.id === props.post.authorId && ( +
+ +
+ ) + ) + } +
{ props.session ? ( - ) : ( - @@ -131,9 +141,9 @@ export default function PostCard( ) }
-
+
- @@ -181,24 +191,33 @@ export default function PostCard(

{props.post.readingTime}

-
+ { + props.post.published && ( + props.session?.id === props.post.authorId && ( +
+ +
+ ) + ) + } +
{ props.session ? ( - ) : ( - ) }
-
+
- diff --git a/components/tags/post-card.tsx b/components/tags/post-card.tsx index 7d60da3..f65718a 100644 --- a/components/tags/post-card.tsx +++ b/components/tags/post-card.tsx @@ -27,6 +27,7 @@ import LoginDialog from "../login-dialog"; import { Skeleton } from "../ui/skeleton"; import { shimmer, toBase64 } from "@/lib/image"; import { validate } from "@/lib/revalidate"; +import PostAnalyticsDialog from "../blog/post-analytics-dialog"; export default function TagPostCard( { className, ...props }: React.ComponentPropsWithoutRef & { @@ -125,16 +126,16 @@ export default function TagPostCard(
-
+
{ props.session ? ( - ) : ( - @@ -143,9 +144,9 @@ export default function TagPostCard( } {formatNumberWithSuffix(props.post._count.likes)}
-
+
- @@ -154,28 +155,36 @@ export default function TagPostCard(
- -
+ { + props.post.published &&( + props.session?.id === props.post.authorId && ( +
+ +
+ ) + ) + } +
{ props.session ? ( - ) : ( - ) }
-
+
- diff --git a/components/tags/users.tsx b/components/tags/users.tsx new file mode 100644 index 0000000..30a4e73 --- /dev/null +++ b/components/tags/users.tsx @@ -0,0 +1,118 @@ +"use client" +import { useSession } from "next-auth/react"; +import FeedPostCard from "../blog/feed-post-card"; +import TagPostCard from "./post-card"; +import { Button } from "../ui/button"; +import Link from "next/link"; +import { useEffect, useState } from "react"; +import UserVerticalCard from "../user-vertical-card"; +import { Swiper, SwiperSlide } from 'swiper/react'; + +// Import Swiper styles +import 'swiper/css'; +import 'swiper/css/pagination'; + + +// import required modules +import { Navigation, Pagination } from 'swiper/modules'; + +export default function TagFollowers({ followers: initialFollowers, tag, session }: { followers: any, tag: any, session: any }) { + const [followers, setFollowers] = useState>(initialFollowers); + useEffect(() => { + setFollowers(initialFollowers) + }, [initialFollowers]) + const settings = { + dots: false, + infinite: false, + draggable: true, + speed: 500, + slidesToShow: 5, + slidesToScroll: 1, + responsive: [ + { + breakpoint: 1024, + settings: { + slidesToShow: 4, + slidesToScroll: 1, + } + }, + { + breakpoint: 600, + settings: { + slidesToShow: 3, + slidesToScroll: 3, + } + }, + { + breakpoint: 480, + settings: { + slidesToShow: 1, + slidesToScroll: 1 + } + } + ] + }; + return ( +
+
+
+

Who to follow

+
+
+ {/*
+ { + followers.map((follower: any) => ( + + )) + } +
*/} + + { + followers.map((follower: any) => ( + + )) + } + + +
+ {/*
+ +
*/} +
+
+ ) +} \ No newline at end of file diff --git a/components/user-horizontal-card.tsx b/components/user-horizontal-card.tsx deleted file mode 100644 index a2e634c..0000000 --- a/components/user-horizontal-card.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import Link from "next/link"; -import { Card, CardContent, CardHeader } from "./ui/card"; -import UserHoverCard from "./user-hover-card"; -import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; -import { Icons } from "./icon"; -import { Button } from "./ui/button"; -import { cn } from "@/lib/utils"; - -export default function UserHorizontalCard({ user, className, ...props }: { user: any } & React.ComponentPropsWithoutRef) { - return ( - - - - - - - {user.name?.charAt(0) || user.username?.charAt(0)} - - - -
- -
- -
-

{user.name || user.username} -

- {user.verified && ( - - )} -
- { - user.bio && ( -
-

{user.bio}

-
- ) - } -
- - -
-
-
- ) -} \ No newline at end of file diff --git a/components/user-vertical-card.tsx b/components/user-vertical-card.tsx new file mode 100644 index 0000000..03dd342 --- /dev/null +++ b/components/user-vertical-card.tsx @@ -0,0 +1,82 @@ +import Link from "next/link"; +import { Card, CardContent, CardHeader } from "./ui/card"; +import UserHoverCard from "./user-hover-card"; +import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; +import { Icons } from "./icon"; +import { Button } from "./ui/button"; +import { cn } from "@/lib/utils"; +import { formatNumberWithSuffix } from "./format-numbers"; +import React from "react"; +import { Plus } from "lucide-react"; +import LoginDialog from "./login-dialog"; + +export default function UserVerticalCard({ user, className, session, ...props }: { user: any, session: any } & React.ComponentPropsWithoutRef) { + const [following, setFollowing] = React.useState(false) + const [followersCount, setFollowersCount] = React.useState(user._count.Followers) + + React.useEffect(() => { + setFollowersCount(user._count.Followers) + setFollowing(user.Followers?.some((follower: any) => follower.followerId === session?.id)) + }, [user, session]) + + const follow = async () => { + const followeeId = user.id + const followerId = session?.id + const res = await fetch(`/api/follow?followeeId=${followeeId}&followerId=${followerId}`) + setFollowing(!following) + setFollowersCount(followersCount + (following ? -1 : 1)) + if (!res.ok) { + setFollowing(!following) + setFollowersCount(followersCount + (following ? -1 : 1)) + } + } + return ( + + +
+ + + + {user.name?.charAt(0) || user.username?.charAt(0)} + + +
+ +
+

{user.name || user.username} +

+ {user.verified && ( + + )} +
+ +

{formatNumberWithSuffix(followersCount)} Followers

+ { + user.bio && ( +
+

{user.bio}

+
+ ) + } +
+
+ + { + session ? ( + + ) : ( + + + + ) + } +
+
+ ) +} \ No newline at end of file diff --git a/lib/prisma/feed.ts b/lib/prisma/feed.ts index 5301fed..d0fbc32 100644 --- a/lib/prisma/feed.ts +++ b/lib/prisma/feed.ts @@ -1,6 +1,7 @@ 'use server' import { getSessionUser } from "@/components/get-session-user"; import postgres from "../postgres"; +import { getFollowings } from "./session"; const getLikes = async ({ id }: { id: string | undefined }) => { const likes = await postgres.like.findMany({ @@ -46,6 +47,65 @@ const getHistory = async ({ id }: { id: string | undefined }) => { return { history: JSON.parse(JSON.stringify(history)) }; } +const getHistoryAuthorPost = async ({ id }: { id: string | undefined }) => { + const historyAuthor = await postgres.readingHistory.findMany({ + where: { userId: id }, + select: { + post: { + select: { + authorId: true, + } + }, + }, + orderBy: { + createdAt: "desc", + }, + take: 10, + }); + + const history = await postgres.post.findMany({ + where: { authorId: { in: historyAuthor.map((history) => history.post.authorId) } }, + select: { + id: true, + }, + orderBy: { + createdAt: "desc", + }, + take: 10, + }); + + return { history: JSON.parse(JSON.stringify(history)) }; +} +const getFollowingsUsers = async ({ id }: { id: string | undefined }) => { + const { followings: sessionFollowingsArray } = await getFollowings({ id }); + console.log(sessionFollowingsArray); + const sessionFollowings = sessionFollowingsArray?.followings?.map((following: any) => following.following); + + const followings = await postgres.tagFollow.findMany({ + where: { followerId: { in: sessionFollowings?.map((following: any) => following.id) } }, + select: { + tagId: true, + }, + orderBy: { + createdAt: "desc", + }, + take: 10, + }); + + const tagIds = followings.map((following) => following.tagId); + + const posts = await postgres.postTag.findMany({ + where: { tagId: { in: tagIds } }, + select: { + postId: true, + }, + orderBy: { + createdAt: "desc", + }, + }); + + return { followings: JSON.parse(JSON.stringify(posts)) }; +} const getTags = async ({ id }: { id: string | undefined }) => { const tags = await postgres.tagFollow.findMany({ @@ -105,8 +165,11 @@ const baseQuery = { savedUsers: { select: { userId: true } }, _count: { select: { - likes: true, - savedUsers: true, + likes: true, + savedUsers: true, + readedUsers: true, + shares: true, + comments: true, }, }, tags: { @@ -126,11 +189,12 @@ export const getForYou = async ({ page = 0, limit = 10 }: { page?: number, limit const { id } = user; //get user's interests - const [{ likes: userLikes }, { bookmarks: userBookmarks }, { history: userHistory }, { postTags: userTags}] = await Promise.all([ + const [{ likes: userLikes }, { bookmarks: userBookmarks }, { history: userHistory }, { postTags: userTags}, { followings: userFollowings }] = await Promise.all([ getLikes({id}), getBookmarks({id}), getHistory({id}), - getTags({id}) + getTags({id}), + getFollowingsUsers({id}), ]); // Fetch the tags of the posts in parallel @@ -142,6 +206,7 @@ export const getForYou = async ({ page = 0, limit = 10 }: { page?: number, limit ...userBookmarks.map((bookmark: any) => bookmark.postId), ...userHistory.map((history: any) => history.postId), ...userTags.map((tag: any) => tag.postId), + ...userFollowings.map((following: any) => following.postId), ], }, }, @@ -161,8 +226,10 @@ const sortedTagIds = Object.entries(tagCounts) .sort((a, b) => b[1] - a[1]) .map(([tagId]) => tagId) + const { history: historyAuthor } = await getHistoryAuthorPost({id}); + const posts = await postgres.post.findMany({ - where: { tags: { some: { tagId: { in: sortedTagIds } } } }, + where: { tags: { some: { tagId: { in: sortedTagIds } } }, id: { in: historyAuthor.map((post: any) => post.id) } }, select: { id: true }, }); diff --git a/lib/prisma/posts.ts b/lib/prisma/posts.ts index 9acba97..c5306f4 100644 --- a/lib/prisma/posts.ts +++ b/lib/prisma/posts.ts @@ -16,6 +16,8 @@ const baseQuery = { likes: true, savedUsers: true, readedUsers: true, + shares: true, + comments: true, }, }, tags: { diff --git a/lib/prisma/session.ts b/lib/prisma/session.ts index 93b5788..bd7db71 100644 --- a/lib/prisma/session.ts +++ b/lib/prisma/session.ts @@ -42,18 +42,23 @@ export const getFollowingTags = async ({ id }: { id: string | undefined }) => { }; export const getFollowings = async ({ id }: { id: string | undefined }) => { - const followings = await postgres.user.findFirst({ - where: { id }, + + if (!id) { + return { followings: [] }; + } + const followings = await postgres.follow.findMany({ + where: { followerId: id }, select: { - Followings: { + following: { include: { - following: true, + Followers: true, + Followings: true, }, }, - id: true, }, }); - return { followings: JSON.parse(JSON.stringify(followings?.Followings)) }; + + return { followings: JSON.parse(JSON.stringify(followings || [])) }; }; export const getFollowers = async ({ id }: { id: string | undefined }) => { @@ -150,6 +155,15 @@ export const getBookmarks = async ({ id, limit = 5, page = 0 }: { id: string | u include: { author: true, savedUsers: true, + _count: { + select: { + likes: true, + savedUsers: true, + readedUsers: true, + shares: true, + comments: true, + }, + }, } }, }, @@ -176,6 +190,15 @@ export const getHistory = async ({ id, limit = 5, page = 0 }: { id: string | und post: { include: { author: true, + _count: { + select: { + likes: true, + savedUsers: true, + readedUsers: true, + shares: true, + comments: true, + }, + }, } }, }, diff --git a/lib/prisma/tags.ts b/lib/prisma/tags.ts index 4142fba..1faf1db 100644 --- a/lib/prisma/tags.ts +++ b/lib/prisma/tags.ts @@ -1,4 +1,6 @@ +import { User } from "@prisma/client"; import postgres from "../postgres"; +import { getFollowings } from "./session"; export const getTags = async ({ id, @@ -155,4 +157,58 @@ export async function getRelatedTags(tagName: string) { ); return { tags: JSON.parse(JSON.stringify(relatedTags))}; +} + +export const getFollowersByUser = async ({ + id, + page = 0, + limit = 10, +}: { + id: string | undefined; + page?: number | undefined; + limit?: number | undefined; +}) => { + const followers = await postgres.tag.findMany({ + where: { followingtag: { some: { followerId: id } } }, + take: limit, + skip: page * limit, + include: { + _count: { select: { posts: true, followingtag: true } }, + followingtag: true, + }, + }); + + return { followers: JSON.parse(JSON.stringify(followers)) }; +} + +export const getFollowersByTag = async ({ + id, + page = 0, + limit = 10, + session, +}: { + id: string | undefined; + page?: number | undefined; + limit?: number | undefined; + session: User["id"] | undefined; +}) => { + const { followings: sessionFollowings } = await getFollowings({ id: session }) + const sessionFollowingIds = sessionFollowings + ? [...sessionFollowings.map((following: any) => following.following.id), session] + : [session]; + const followers = await postgres.tagFollow.findMany({ + where: { tagId: id, ...(session && { followerId: { not: { in: sessionFollowingIds } } }) }, + take: limit, + skip: page * limit, + select: { + follower: { + include: { + _count: { select: { posts: true, Followers: true } }, + Followers: true, + }, + }, + } + }); + + return { followers: JSON.parse(JSON.stringify(followers)) }; } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3dc770d..e3c3a0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,7 +94,7 @@ "slate-history": "^0.93.0", "slate-hyperscript": "^0.77.0", "slate-react": "^0.98.4", - "swiper": "^11.0.2", + "swiper": "^11.0.5", "tailwind-merge": "^1.14.0", "tailwindcss": "3.3.3", "tailwindcss-animate": "^1.0.7", @@ -11940,9 +11940,9 @@ } }, "node_modules/swiper": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/swiper/-/swiper-11.0.2.tgz", - "integrity": "sha512-JMHZYdUDG0V5ZdzWJkQicW4F7u4edmS4vlOhciTDhcZokDL2N8EE2uP4INxqIgpiJMoeHlwATqZk2yEAW7F6Dw==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-11.0.5.tgz", + "integrity": "sha512-rhCwupqSyRnWrtNzWzemnBLMoyYuoDgGgspAm/8iBD3jCvAWycPLH4Z3TB0O5520DHLzMx94yUMH/B9Efpa48w==", "funding": [ { "type": "patreon", diff --git a/package.json b/package.json index 4cc0482..e8c5516 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "slate-history": "^0.93.0", "slate-hyperscript": "^0.77.0", "slate-react": "^0.98.4", - "swiper": "^11.0.2", + "swiper": "^11.0.5", "tailwind-merge": "^1.14.0", "tailwindcss": "3.3.3", "tailwindcss-animate": "^1.0.7",