Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Frontend/Backend/DB: Notification support for like, comment and quiz reminders #115

Merged
merged 2 commits into from
Oct 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 33 additions & 21 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -40,25 +40,25 @@ model Session {
}

model User {
id String @id @default(cuid())
id String @id @default(cuid())
name String?
email String? @unique
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
questions Question[]
likes Like[]
comments Comment[]
followers Follow[] @relation("followings")
followings Follow[] @relation("followers")
points Int @default(0)
followers Follow[] @relation("followings")
followings Follow[] @relation("followers")
points Int @default(0)
progress Progress[]
notifications Notification[]

@@fulltext([name, email])
}

//TODO: implement Question model and relate to the Progress
model Progress {
lastEvaluated DateTime @updatedAt
interval Float @default(0)
Expand All @@ -83,21 +83,22 @@ model VerificationToken {

// Main models
model Question {
id String @id @default(cuid())
caption String @db.Text
videoURL String @db.Text
coverURL String @db.Text
videoWidth Int
videoHeight Int
userId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
likes Like[]
comments Comment[]
hashtags Hashtag[]
progress Progress[]
contentType Int @default(3) // 1: image, 2: video, 3: unknown
id String @id @default(cuid())
caption String @db.Text
videoURL String @db.Text
coverURL String @db.Text
videoWidth Int
videoHeight Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
likes Like[]
comments Comment[]
hashtags Hashtag[]
progress Progress[]
notifications Notification[]
contentType Int @default(3) // 1: image, 2: video, 3: unknown
userId String
user User @relation(fields: [userId], references: [id])

@@fulltext([caption])
}
Expand All @@ -117,6 +118,17 @@ model Like {
@@id([questionId, userId])
}

model Notification {
id Int @id @default(autoincrement())
userId String // who are we notifying
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
content String // e.g., you have a new like from user <name>
status Int @default(0) // 1: checked, 0: unchecked
questionId String @default("")
questions Question @relation(fields: [questionId], references: [id], onDelete: Cascade)
lastUpdated DateTime @updatedAt
}

model Comment {
id String @id @default(cuid())
questionId String
Expand Down
33 changes: 22 additions & 11 deletions src/components/Home/QuestionSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const QuestionSection: FC<QuestionSectionProps> = ({ question, refetch, origin }
const session = useSession();

const likeMutation = trpc.useMutation("like.toggle");
const notificationMutation = trpc.useMutation("notification.createLike");
const followMutation = trpc.useMutation("follow.toggle");

const [isCurrentlyLiked, setIsCurrentlyLiked] = useState(question.likedByMe);
Expand All @@ -48,6 +49,18 @@ const QuestionSection: FC<QuestionSectionProps> = ({ question, refetch, origin }
if (!session.data?.user) {
toast("You need to login");
} else {
try {
if (!isCurrentlyLiked) {
notificationMutation.mutateAsync({
content: session.data.user.name + " liked your post ",
questionId: question.id as string,
userId: question.user.id as string,
});
}
} catch {
throw new Error("Cannot update notifications");
}

likeMutation
.mutateAsync({
isLiked: !isCurrentlyLiked,
Expand Down Expand Up @@ -120,15 +133,14 @@ const QuestionSection: FC<QuestionSectionProps> = ({ question, refetch, origin }
</p>
</div>
{/* @ts-ignore */}
{question.userId !== session.data?.user.id && (
{question.userId !== session.data?.user?.id && (
<div className="flex-shrink-0">
<button
onClick={() => toggleFollow()}
className={`py-1 px-3 rounded text-sm mt-2 ${
isCurrentlyFollowed ?? question.followedByMe
? "border hover:bg-[#F8F8F8] transition"
: "border border-pink text-pink hover:bg-[#FFF4F5] transition"
}`}
className={`py-1 px-3 rounded text-sm mt-2 ${isCurrentlyFollowed ?? question.followedByMe
? "border hover:bg-[#F8F8F8] transition"
: "border border-pink text-pink hover:bg-[#FFF4F5] transition"
}`}
>
{isCurrentlyFollowed ?? question.followedByMe
? "Following"
Expand All @@ -140,11 +152,10 @@ const QuestionSection: FC<QuestionSectionProps> = ({ question, refetch, origin }
<div className="flex items-end gap-5">
<Link href={`/question/${question.id}`}>
<a
className={`${
question.videoHeight > question.videoWidth * 1.3
? "md:h-[600px]"
: "flex-grow h-auto"
} block bg-[#3D3C3D] rounded-md overflow-hidden flex-grow h-auto md:flex-grow-0`}
className={`${question.videoHeight > question.videoWidth * 1.3
? "md:h-[600px]"
: "flex-grow h-auto"
} block bg-[#3D3C3D] rounded-md overflow-hidden flex-grow h-auto md:flex-grow-0`}
>
<VideoPlayer
src={question.videoURL}
Expand Down
107 changes: 57 additions & 50 deletions src/components/Layout/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useEffect } from "react";
import Image from "next/future/image";
import Link from "next/link";
import { useRouter } from "next/router";
Expand All @@ -11,11 +12,14 @@ import ClickAwayListener from "../Shared/ClickAwayListener";

import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import { trpc } from "@/utils/trpc";

const Navbar: FC = () => {
const [subjectValue, setSubjectValue] = useState<string[]>([]);
const [chapterValue, setChapterValue] = useState<string[]>([]);

const [notifications, setNotifications] = useState<{ content: string[], status: number[] }>({ content: [], status: [] });

const subjects = [
'#Biology ',
'#History ',
Expand All @@ -29,12 +33,12 @@ const Navbar: FC = () => {
];

const router = useRouter();
const notificationMutation = trpc.useMutation("notification.for-you");

const { data: session, status } = useSession();

const [isDropdownOpened, setIsDropdownOpened] = useState(false);

const [notifications, setNotifications] = useState<string[]>([]);

const [notificationVisibility, setNotificationVisibility] = useState(false);

Expand All @@ -44,6 +48,12 @@ const Navbar: FC = () => {
: ""
);

useEffect(()=>{
notificationMutation.mutateAsync().then(notifs => {
console.log(notifs);
setNotifications(notifs)});
}, []);

const handleFormSubmit = (e: FormEvent) => {
e.preventDefault();

Expand All @@ -52,15 +62,18 @@ const Navbar: FC = () => {
}
};

const displayNotification = (n: string) => {
console.log(notifications);
if (n == "Quiz") {
return <Link href={`/quizUltimate`}>Go to Quiz Page</Link>
} else if (n == "Like") {
return <span className="notification">Someone liked your post</span>
} else {
return <span className="notification">Someone commented under your post</span>
}

const displayNotification = () => {
const contentLst: string[] = notifications.content;
const statusLst: number[] = notifications.status;

return contentLst.map((n) => {
if (n == "Quiz") {
return <Link href={`/quizUltimate`}>Go to Quiz Page</Link>
} else {
return <span className="notification">{n}</span>
}
})
}

return (
Expand All @@ -75,38 +88,38 @@ const Navbar: FC = () => {
</a>
</Link>
<div className='flex space-x-2 min-w-[30%]'>
<Autocomplete
value={subjectValue}
onChange={(event, newValue) => {
setSubjectValue(newValue);
}}
options={subjects}
multiple
limitTags={2}
id="caption"
className="p-1 w-full mt-1 mb-3 outline-none focus:border-gray-400 transition"
style={{ paddingTop: 20, paddingBottom: 20, maxHeight: 90 }}
renderInput={(params) => (
<TextField {...params} label="Subject" placeholder="Biology, History, Spanish ..." />
)}
sx={{ width: '1/2' }}
/>
<Autocomplete
value={chapterValue}
onChange={(event, newValue) => {
setChapterValue(newValue);
}}
options={chapters}
multiple
limitTags={2}
id="caption"
className="p-1 w-full mt-1 mb-3 outline-none focus:border-gray-400 transition"
style={{ paddingTop: 20, maxHeight: 90 }}
renderInput={(params) => (
<TextField {...params} label="Chapters" placeholder="Chapter 1, 2 ..." />
)}
sx={{ width: '1/2', height: '10' }}
/>
<Autocomplete
value={subjectValue}
onChange={(event, newValue) => {
setSubjectValue(newValue);
}}
options={subjects}
multiple
limitTags={2}
id="caption"
className="p-1 w-full mt-1 mb-3 outline-none focus:border-gray-400 transition"
style={{ paddingTop: 20, paddingBottom: 20, maxHeight: 90 }}
renderInput={(params) => (
<TextField {...params} label="Subject" placeholder="Biology, History, Spanish ..." />
)}
sx={{ width: '1/2' }}
/>
<Autocomplete
value={chapterValue}
onChange={(event, newValue) => {
setChapterValue(newValue);
}}
options={chapters}
multiple
limitTags={2}
id="caption"
className="p-1 w-full mt-1 mb-3 outline-none focus:border-gray-400 transition"
style={{ paddingTop: 20, maxHeight: 90 }}
renderInput={(params) => (
<TextField {...params} label="Chapters" placeholder="Chapter 1, 2 ..." />
)}
sx={{ width: '1/2', height: '10' }}
/>
</div>
<form
onSubmit={handleFormSubmit}
Expand All @@ -128,16 +141,10 @@ const Navbar: FC = () => {
</button>
</form>
<div className="flex items-center gap-3">
<div className="cursor-pointer" onClick={async () => { setNotifications(notifications.concat(["Like"])); }}>
<span >Add like notification</span></div>
<div className="cursor-pointer" onClick={async () => { setNotifications(notifications.concat(["Comment"])); }}>
<span >Add comment notification</span></div>
<div className="cursor-pointer" onClick={async () => { setNotifications(notifications.concat(["Quiz"])); }}>
<span >Add quiz notification</span></div>
<div className="notificationArea border rounded" onClick={async () => { setNotificationVisibility(!notificationVisibility); }}>
<img src="/notificationBell.svg" className="notificationBell"></img>
<div className="notificationCounter">{notifications.length}</div>
{notificationVisibility && <div className="notifications" id="notifications">{notifications.map((n) => displayNotification(n))}</div>}
<div className="notificationCounter">{notifications.content.length}</div>
{notificationVisibility && <div className="notifications" id="notifications">{displayNotification()}</div>}
</div>
<Link href={status === "authenticated" ? "/create-mnemonics" : "/sign-in"}>
<a className="border rounded flex items-center gap-2 h-9 px-3 border-gray-200 bg-white hover:bg-gray-100 transition">
Expand Down
32 changes: 20 additions & 12 deletions src/pages/question/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const Question: NextPage<QuestionProps> = ({ question, href, title }) => {
const likeMutation = trpc.useMutation("like.toggle");
const followMutation = trpc.useMutation("follow.toggle");
const postCommentMutation = trpc.useMutation("comment.post");
const notificationMutation = trpc.useMutation("notification.createComment");

const [isCurrentlyLiked, setIsCurrentlyLiked] = useState(question?.likedByMe);
const [isCurrentlyFollowed, setIsCurrentlyFollowed] = useState(
Expand Down Expand Up @@ -103,6 +104,17 @@ const Question: NextPage<QuestionProps> = ({ question, href, title }) => {
console.log(err);
toast.error("Post comment failed");
});
try {
notificationMutation.mutateAsync({
content: session.data?.user?.name + " commented your post " + "\"" + inputValue.trim() + "\"",
questionId: question?.id as string,
userId: question?.user.id as string,
});
} catch {
throw new Error("Cannot update notifications");
}


};

if (!question) return <></>;
Expand Down Expand Up @@ -179,11 +191,10 @@ const Question: NextPage<QuestionProps> = ({ question, href, title }) => {
<div className="flex-shrink-0">
<button
onClick={() => toggleFollow()}
className={`py-1 px-3 rounded text-sm mt-2 ${
isCurrentlyFollowed ?? question.followedByMe
className={`py-1 px-3 rounded text-sm mt-2 ${isCurrentlyFollowed ?? question.followedByMe
? "border hover:bg-[#F8F8F8] transition"
: "border border-pink text-pink hover:bg-[#FFF4F5] transition"
}`}
}`}
>
{isCurrentlyFollowed ?? question.followedByMe
? "Following"
Expand All @@ -207,9 +218,8 @@ const Question: NextPage<QuestionProps> = ({ question, href, title }) => {
className="w-9 h-9 bg-[#F1F1F2] fill-black flex justify-center items-center rounded-full"
>
<AiFillHeart
className={`w-5 h-5 ${
isCurrentlyLiked ? "fill-pink" : ""
}`}
className={`w-5 h-5 ${isCurrentlyLiked ? "fill-pink" : ""
}`}
/>
</button>
<span className="text-center text-xs font-semibold">
Expand Down Expand Up @@ -326,11 +336,10 @@ const Question: NextPage<QuestionProps> = ({ question, href, title }) => {
<button
disabled={postCommentMutation.isLoading || !inputValue.trim()}
type="submit"
className={`transition ${
postCommentMutation.isLoading || !inputValue.trim()
className={`transition ${postCommentMutation.isLoading || !inputValue.trim()
? ""
: "text-pink"
}`}
}`}
>
{postCommentMutation.isLoading ? "Posting..." : "Post"}
</button>
Expand Down Expand Up @@ -407,9 +416,8 @@ export const getServerSideProps = async ({
followedByMe,
},
session,
href: `${
req.headers.host?.includes("localhost") ? "http" : "https"
}://${req.headers.host}/question/${id}`,
href: `${req.headers.host?.includes("localhost") ? "http" : "https"
}://${req.headers.host}/question/${id}`,
title: `${question.user.name} on EdTok`,
},
};
Expand Down
Loading