Skip to content

Commit

Permalink
Favoriting Functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
SashenJayathilaka committed Jun 9, 2024
1 parent 7eff60c commit 8c31be2
Show file tree
Hide file tree
Showing 11 changed files with 283 additions and 8 deletions.
10 changes: 9 additions & 1 deletion app/(dashboard)/_components/board-card/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ function Footer({
onClick,
disabled,
}: Props) {
const handleClick = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
event.stopPropagation();
event.preventDefault();
onClick();
};

return (
<div className="relative bg-white p-3">
<p className="text-[13px] truncate max-w-[calc(100%-20px)]">{tittle}</p>
Expand All @@ -26,7 +34,7 @@ function Footer({
</p>
<button
disabled={disabled}
onClick={onClick}
onClick={handleClick}
className={cn(
"opacity-0 group-hover:opacity-100 transition absolute top-3 right-3 text-muted-foreground hover:text-blue-600",
disabled && "cursor-not-allowed opacity-75"
Expand Down
27 changes: 25 additions & 2 deletions app/(dashboard)/_components/board-card/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import Actions from "@/components/actions";
import { Skeleton } from "@/components/ui/skeleton";
import { api } from "@/convex/_generated/api";
import { useApiMutation } from "@/hooks/use-api-mutations";
import { useAuth } from "@clerk/nextjs";
import { formatDistanceToNow } from "date-fns";
import { MoreHorizontal } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { toast } from "sonner";
import Overlay from "./Overlay";
import Footer from "./footer";

Expand Down Expand Up @@ -37,6 +40,26 @@ export function BoardCard({
addSuffix: true,
});

/* const handleFavorite = useMutation(api.board.favorite);
const handleUnfavorite = useMutation(api.board.unfavorite) */

const { mutate: onFavorite, pending: pendingFavorite } = useApiMutation(
api.board.favorite
);
const { mutate: onUnfavorite, pending: pendingUnfavorite } = useApiMutation(
api.board.unfavorite
);

const toggleFavorite = () => {
if (isFavorite) {
onUnfavorite({ id }).catch((error) =>
toast.error("Failed To Unfavorite")
);
} else {
onFavorite({ id, orgId }).catch(() => toast.error("Failed To Favorite"));
}
};

return (
<Link href={`/board/${id}`}>
<div className="group aspect-[100/127] border rounded-lg flex flex-col justify-between overflow-hidden">
Expand All @@ -54,8 +77,8 @@ export function BoardCard({
authorLabel={authorLabel}
createdAtLabel={createAtLabel}
isFavorite={isFavorite}
onClick={() => {}}
disabled={false}
onClick={toggleFavorite}
disabled={pendingFavorite || pendingUnfavorite}
/>
</div>
</Link>
Expand Down
2 changes: 1 addition & 1 deletion app/(dashboard)/_components/board-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ function BoardList({ orgId, query }: Props) {
createdAt={board._creationTime}
imageUrl={board.imageUrl}
orgId={board.orgId}
isFavorite={false}
isFavorite={board.isFavorite}
/>
))}
</div>
Expand Down
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Inter } from "next/font/google";
import "./globals.css";
import { ConvexClientProvider } from "@/providers/convex-client-provider";
import { Toaster } from "@/components/ui/sonner";
import ModelProvider from "@/providers/model-provider";

const inter = Inter({ subsets: ["latin"] });

Expand All @@ -21,6 +22,7 @@ export default function RootLayout({
<body className={inter.className}>
<ConvexClientProvider>
<Toaster />
<ModelProvider />
{children}
</ConvexClientProvider>
</body>
Expand Down
13 changes: 11 additions & 2 deletions components/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@

import { api } from "@/convex/_generated/api";
import { useApiMutation } from "@/hooks/use-api-mutations";
import { UseRenameModal } from "@/store/use-rename-modal";
import { DropdownMenuContentProps } from "@radix-ui/react-dropdown-menu";
import { Link2, Trash2 } from "lucide-react";
import { Link2, Pencil, Trash2 } from "lucide-react";
import React from "react";
import { toast } from "sonner";
import ConfirmModel from "./confirm-model";
import { Button } from "./ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "./ui/dropdown-menu";
import { Button } from "./ui/button";

type Props = {
children: React.ReactNode;
Expand All @@ -24,6 +25,7 @@ type Props = {
};

function Actions({ children, side, id, tittle, sideOffset }: Props) {
const { onOpen } = UseRenameModal();
const { mutate, pending } = useApiMutation(api.board.remove);

const onDelete = () => {
Expand Down Expand Up @@ -52,6 +54,13 @@ function Actions({ children, side, id, tittle, sideOffset }: Props) {
<Link2 className="h-4 w-4 mr-2" />
Copy Board Link
</DropdownMenuItem>
<DropdownMenuItem
className="p-3 cursor-pointer"
onClick={() => onOpen(id, tittle)}
>
<Pencil className="h-4 w-4 mr-2" />
Rename
</DropdownMenuItem>
<ConfirmModel
onConfirm={onDelete}
header="Delete Board?"
Expand Down
79 changes: 79 additions & 0 deletions components/modal/rename-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"use client";

import { api } from "@/convex/_generated/api";
import { useApiMutation } from "@/hooks/use-api-mutations";
import { UseRenameModal } from "@/store/use-rename-modal";
import { FormEventHandler, useEffect, useState } from "react";
import { toast } from "sonner";
import { Button } from "../ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "../ui/dialog";
import { Input } from "../ui/input";

type Props = {};

function RenameModal({}: Props) {
const { mutate, pending } = useApiMutation(api.board.update);
const { isOpen, onClose, initialValues } = UseRenameModal();
const [title, setTitle] = useState(initialValues.title);

useEffect(() => {
setTitle(initialValues.title);
}, [initialValues.title]);

const onSubmit: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();

mutate({
id: initialValues.id,
title,
})
.then(() => {
toast.success("Board Rename");
onClose();
})
.catch((error) => {
toast.error("failed To Rename To Board");
});
};

return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Board Tittle</DialogTitle>
</DialogHeader>
<DialogDescription>Enter New Tittle for this board</DialogDescription>
<form className="space-y-4" onSubmit={onSubmit}>
<Input
disabled={pending}
required
maxLength={60}
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Board Title"
/>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">
Cancel
</Button>
</DialogClose>
<Button disabled={pending} type="submit">
Save
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

export default RenameModal;
79 changes: 78 additions & 1 deletion convex/board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,81 @@ export const remove = mutation({
},
});

/* 3.35 */
export const update = mutation({
args: { id: v.id("boards"), title: v.string() },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();

if (!identity) {
throw new Error("Unauthorized");
}

const title = args.title.trim();

if (!title) {
throw new Error("Tittle is required");
}
if (title.length > 60) {
throw new Error("Tittle cannot longer than 60 characters");
}

const board = await ctx.db.patch(args.id, {
title: args.title,
});

return board;
},
});

export const favorite = mutation({
args: { id: v.id("boards"), orgId: v.string() },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Unauthorized");
const board = await ctx.db.get(args.id);
if (!board) {
throw new Error("Board Not Found");
}
const userId = identity.subject;
const exitingFavorite = await ctx.db
.query("userFavorites")
.withIndex("by_user_board_org", (q) =>
q.eq("userId", userId).eq("boardId", board._id).eq("orgId", args.orgId)
)
.unique();
if (exitingFavorite) {
throw new Error("Board Already Favorite");
}
await ctx.db.insert("userFavorites", {
userId,
boardId: board._id,
orgId: args.orgId,
});
return board;
},
});

export const unfavorite = mutation({
args: { id: v.id("boards") },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Unauthorized");
const board = await ctx.db.get(args.id);
if (!board) {
throw new Error("Board Not Found");
}
const userId = identity.subject;
const exitingFavorite = await ctx.db
.query("userFavorites")
.withIndex(
"by_user_board",
(q) => q.eq("userId", userId).eq("boardId", board._id) //TODO:
)
.unique();
if (!exitingFavorite) {
throw new Error("Favorite Board Not Found");
}
await ctx.db.delete(exitingFavorite._id);
return board;
},
});
19 changes: 18 additions & 1 deletion convex/boards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,23 @@ export const get = query({
.order("desc")
.collect();

return boards;
const boardsWithFavoriteRelation = boards.map((board) => {
return ctx.db
.query("userFavorites")
.withIndex("by_user_board", (q) =>
q.eq("userId", identity.subject).eq("boardId", board._id)
)
.unique()
.then((favorite) => {
return {
...board,
isFavorite: !!favorite,
};
});
});

const boardsWithFavoriteBoolean = Promise.all(boardsWithFavoriteRelation);

return boardsWithFavoriteBoolean;
},
});
9 changes: 9 additions & 0 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,13 @@ export default defineSchema({
searchField: "title",
filterFields: ["orgId"],
}),
userFavorites: defineTable({
orgId: v.string(),
userId: v.string(),
boardId: v.id("boards"),
})
.index("by_board", ["boardId"])
.index("by_user_org", ["userId", "orgId"])
.index("by_user_board", ["userId", "boardId"])
.index("by_user_board_org", ["userId", "boardId", "orgId"]),
});
26 changes: 26 additions & 0 deletions providers/model-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client";

import RenameModal from "@/components/modal/rename-modal";
import { useEffect, useState } from "react";

type Props = {};

function ModelProvider({}: Props) {
const [isMounted, setIsMounted] = useState(false);

useEffect(() => {
setIsMounted(true);
}, []);

if (!isMounted) {
return null;
}

return (
<>
<RenameModal />
</>
);
}

export default ModelProvider;
Loading

0 comments on commit 8c31be2

Please sign in to comment.