-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7db2817
commit 8a7826c
Showing
8 changed files
with
329 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { getProfile } from "./users"; | ||
import { authMutation, authQuery } from "./util"; | ||
|
||
export const markAllRead = authMutation({ | ||
args: {}, | ||
handler: async (ctx) => { | ||
const unreadNotifications = await ctx.db | ||
.query("notifications") | ||
.withIndex("by_userId", (q) => q.eq("userId", ctx.user._id)) | ||
.filter((q) => q.eq(q.field("isRead"), false)) | ||
.collect(); | ||
|
||
await Promise.all( | ||
unreadNotifications.map(async (notification) => { | ||
return await ctx.db.patch(notification._id, { | ||
isRead: true, | ||
}); | ||
}) | ||
); | ||
}, | ||
}); | ||
|
||
export const hasUnread = authQuery({ | ||
args: {}, | ||
handler: async (ctx) => { | ||
if (!ctx.user) return false; | ||
|
||
const unreadNotifications = await ctx.db | ||
.query("notifications") | ||
.withIndex("by_userId", (q) => q.eq("userId", ctx.user._id)) | ||
.filter((q) => q.eq(q.field("isRead"), false)) | ||
.collect(); | ||
|
||
return unreadNotifications.length > 0; | ||
}, | ||
}); | ||
|
||
export const getNotifications = authQuery({ | ||
args: {}, | ||
handler: async (ctx) => { | ||
if (!ctx.user) return []; | ||
|
||
const notifications = await ctx.db | ||
.query("notifications") | ||
.filter((q) => q.eq(q.field("userId"), ctx.user._id)) | ||
.order("desc") | ||
.collect(); | ||
|
||
return Promise.all( | ||
notifications.map(async (notification) => { | ||
return { | ||
...notification, | ||
thumbnail: await ctx.db.get(notification.thumbnailId), | ||
profile: await getProfile(ctx, { userId: notification.userId }), | ||
}; | ||
}) | ||
); | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
"use client"; | ||
|
||
import { useMutation, useQuery } from "convex/react"; | ||
import { api } from "../../../convex/_generated/api"; | ||
import { Button } from "@/components/ui/button"; | ||
import Image from "next/image"; | ||
import { useSession } from "@/lib/utils"; | ||
import Link from "next/link"; | ||
import { SkeletonCard } from "@/components/skeleton-card"; | ||
import { PictureInPictureIcon, SpeechIcon } from "lucide-react"; | ||
import { Id } from "../../../convex/_generated/dataModel"; | ||
import { ReactNode, useEffect } from "react"; | ||
import { timeFrom } from "@/util/time-from"; | ||
|
||
function Notification({ | ||
notification, | ||
title, | ||
description, | ||
icon, | ||
}: { | ||
notification: { | ||
_creationTime: number; | ||
_id: Id<"notifications">; | ||
from: Id<"users">; | ||
thumbnailId: Id<"thumbnails">; | ||
profile: { | ||
name: string | undefined; | ||
}; | ||
}; | ||
title: string; | ||
description: string; | ||
icon: ReactNode; | ||
}) { | ||
return ( | ||
<div | ||
className="flex items-center gap-4 border p-4 rounded" | ||
key={notification._id} | ||
> | ||
{icon} | ||
|
||
<div> | ||
<div className="font-bold mb-2">{title}</div> | ||
<div className="mb-2">{timeFrom(notification._creationTime)}</div> | ||
<div> | ||
<Link href={`/profile/${notification.from}`}> | ||
{notification.profile.name}{" "} | ||
</Link> | ||
{description} | ||
</div> | ||
</div> | ||
|
||
<Button asChild className="ml-auto"> | ||
<Link href={`/thumbnails/${notification.thumbnailId}`}>View</Link> | ||
</Button> | ||
</div> | ||
); | ||
} | ||
|
||
export default function NotificationsPage() { | ||
const { isAuthenticated } = useSession(); | ||
|
||
const notifications = useQuery( | ||
api.notification.getNotifications, | ||
!isAuthenticated ? "skip" : undefined | ||
); | ||
|
||
const markAllRead = useMutation(api.notification.markAllRead); | ||
|
||
useEffect(() => { | ||
markAllRead(); | ||
}, [markAllRead]); | ||
|
||
return ( | ||
<div className=""> | ||
<h1 className="text-center text-4xl font-bold mb-12">Notifications</h1> | ||
|
||
{notifications === undefined && ( | ||
<div className="animate-pulse mb-12 mt-12 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8"> | ||
<SkeletonCard /> | ||
<SkeletonCard /> | ||
<SkeletonCard /> | ||
</div> | ||
)} | ||
|
||
{notifications && notifications.length === 0 && ( | ||
<div className="flex flex-col items-center gap-8"> | ||
<Image | ||
className="rounded-lg bg-white p-12" | ||
src="/void.svg" | ||
alt="no found icon" | ||
width="400" | ||
height="400" | ||
/> | ||
<div className="text-2xl font-bold">You have no notifications</div> | ||
</div> | ||
)} | ||
|
||
<div className="flex flex-col gap-8 max-w-xl mx-auto"> | ||
{notifications?.map((notification) => { | ||
if (notification.type === "thumbnail") { | ||
return ( | ||
<Notification | ||
key={notification._id} | ||
description=" uploaded a new thumbnail test!" | ||
icon={<PictureInPictureIcon className="h-14 w-14" />} | ||
title="New Thumbnail" | ||
notification={notification} | ||
/> | ||
); | ||
} else if (notification.type === "comment") { | ||
return ( | ||
<Notification | ||
key={notification._id} | ||
description=" left a comment on your thumbnail." | ||
icon={<SpeechIcon className="h-14 w-14" />} | ||
title="New Comment" | ||
notification={notification} | ||
/> | ||
); | ||
} else { | ||
return ( | ||
<Notification | ||
key={notification._id} | ||
description=" voted for one of your thumbnail images." | ||
icon={<PictureInPictureIcon className="h-14 w-14" />} | ||
title="New Vote" | ||
notification={notification} | ||
/> | ||
); | ||
} | ||
})} | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import * as React from "react" | ||
import { cva, type VariantProps } from "class-variance-authority" | ||
|
||
import { cn } from "@/lib/utils" | ||
|
||
const alertVariants = cva( | ||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", | ||
{ | ||
variants: { | ||
variant: { | ||
default: "bg-background text-foreground", | ||
destructive: | ||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", | ||
}, | ||
}, | ||
defaultVariants: { | ||
variant: "default", | ||
}, | ||
} | ||
) | ||
|
||
const Alert = React.forwardRef< | ||
HTMLDivElement, | ||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants> | ||
>(({ className, variant, ...props }, ref) => ( | ||
<div | ||
ref={ref} | ||
role="alert" | ||
className={cn(alertVariants({ variant }), className)} | ||
{...props} | ||
/> | ||
)) | ||
Alert.displayName = "Alert" | ||
|
||
const AlertTitle = React.forwardRef< | ||
HTMLParagraphElement, | ||
React.HTMLAttributes<HTMLHeadingElement> | ||
>(({ className, ...props }, ref) => ( | ||
<h5 | ||
ref={ref} | ||
className={cn("mb-1 font-medium leading-none tracking-tight", className)} | ||
{...props} | ||
/> | ||
)) | ||
AlertTitle.displayName = "AlertTitle" | ||
|
||
const AlertDescription = React.forwardRef< | ||
HTMLParagraphElement, | ||
React.HTMLAttributes<HTMLParagraphElement> | ||
>(({ className, ...props }, ref) => ( | ||
<div | ||
ref={ref} | ||
className={cn("text-sm [&_p]:leading-relaxed", className)} | ||
{...props} | ||
/> | ||
)) | ||
AlertDescription.displayName = "AlertDescription" | ||
|
||
export { Alert, AlertTitle, AlertDescription } |
Oops, something went wrong.