From b21de1ed194beb18904de51d1e23c195052afadd Mon Sep 17 00:00:00 2001 From: Nishant Racherla Date: Mon, 15 Jan 2024 19:03:53 -0800 Subject: [PATCH] Add addLike action, modularize LikeButton, fix multiple likes from same user --- app/(recipes)/actions.ts | 44 ++++++++++++++++++++++++++- app/(recipes)/recipes/[item]/page.tsx | 9 ++---- app/(recipes)/recipes/like-button.tsx | 30 ++++++++++++++++++ app/db.ts | 27 ++++++++++------ 4 files changed, 93 insertions(+), 17 deletions(-) create mode 100644 app/(recipes)/recipes/like-button.tsx diff --git a/app/(recipes)/actions.ts b/app/(recipes)/actions.ts index f7fb97f..6988f83 100644 --- a/app/(recipes)/actions.ts +++ b/app/(recipes)/actions.ts @@ -1,11 +1,20 @@ "use server"; import z from "zod"; -import { db, recipesTable, genRecipeId, usersTable } from "@/app/db"; +import { + db, + recipesTable, + genRecipeId, + usersTable, + likesTable, + genLikeId, +} from "@/app/db"; import { auth } from "@/app/auth"; import { redirect } from "next/navigation"; import { newRecipeRateLimit } from "@/lib/rate-limit"; import { put } from "@vercel/blob"; +import { eq } from "drizzle-orm"; +import { revalidatePath } from "next/cache"; const MAX_FILE_SIZE = 400000; const ACCEPTED_IMAGE_TYPES = [ @@ -133,3 +142,36 @@ export async function submitRecipe( redirect(`/recipes/${id.replace(/^recipe_/, "")}`); } + +export async function addLike(recipeId: string, likes: number) { + const session = await auth(); + + if (!session?.user?.id) redirect("/login"); + + const userId = session.user.id; + + try { + const id = genLikeId(); + + // add entry to likes table + await db + .insert(likesTable) + .values({ id, recipe_id: recipeId, user_id: userId }); + + // update recipe likes column + await db + .update(recipesTable) + .set({ likes: likes + 1 }) + .where(eq(recipesTable.id, recipeId)); + } catch (err) { + console.error(err); + return { + error: { + code: "INTERNAL_ERROR", + message: "Failed to like recipe. Please try again later.", + }, + }; + } + + revalidatePath(`/recipes/${recipeId.replace(/^recipe_/, "")}`); +} diff --git a/app/(recipes)/recipes/[item]/page.tsx b/app/(recipes)/recipes/[item]/page.tsx index 56a98b9..7843410 100644 --- a/app/(recipes)/recipes/[item]/page.tsx +++ b/app/(recipes)/recipes/[item]/page.tsx @@ -1,10 +1,10 @@ import { db, recipesTable } from "@/app/db"; import { sql } from "drizzle-orm"; -import { Heart } from "lucide-react"; import { nanoid } from "nanoid"; import { headers } from "next/headers"; import Image from "next/image"; import { notFound } from "next/navigation"; +import LikeButton from "../like-button"; async function getRecipe(id: string) { const recipeId = `recipe_${id}`; @@ -45,7 +45,7 @@ export default async function RecipeItem({ /> )} -
+

Cuisine: @@ -60,10 +60,7 @@ export default async function RecipeItem({ {recipe.prepTime} min

-

- - {recipe.likes} -

+

Ingredients

diff --git a/app/(recipes)/recipes/like-button.tsx b/app/(recipes)/recipes/like-button.tsx new file mode 100644 index 0000000..c9123b6 --- /dev/null +++ b/app/(recipes)/recipes/like-button.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { useOptimistic } from "react"; +import { addLike } from "../actions"; +import { Heart } from "lucide-react"; + +interface LikeButtonProps { + likes: number; + recipeId: string; +} + +export default function LikeButton({ likes, recipeId }: LikeButtonProps) { + const [optimisticLikes, addOptimisticLike] = useOptimistic( + likes, + (state, _) => state + 1, + ); + + return ( +
+ { + addOptimisticLike(1); + await addLike(recipeId, optimisticLikes); + }} + /> + {Number(optimisticLikes)} +
+ ); +} diff --git a/app/db.ts b/app/db.ts index b5a4ea2..faf781f 100644 --- a/app/db.ts +++ b/app/db.ts @@ -7,6 +7,7 @@ import { integer, varchar, timestamp, + unique, } from "drizzle-orm/pg-core"; import { customAlphabet } from "nanoid"; import { nolookalikes } from "nanoid-dictionary"; @@ -83,16 +84,22 @@ export const genRecipeId = () => { return `recipe_${nanoid(12)}`; }; -export const likesTable = pgTable("likes", { - id: varchar("id", { length: 256 }).primaryKey().notNull(), - recipe_id: varchar("recipe_id", { length: 256 }) - .notNull() - .references(() => recipesTable.id), - user_id: varchar("user_id", { length: 256 }) - .notNull() - .references(() => usersTable.id), -}); +export const likesTable = pgTable( + "likes", + { + id: varchar("id", { length: 256 }).primaryKey().notNull(), + recipe_id: varchar("recipe_id", { length: 256 }) + .notNull() + .references(() => recipesTable.id), + user_id: varchar("user_id", { length: 256 }) + .notNull() + .references(() => usersTable.id), + }, + (t) => ({ + unq: unique().on(t.recipe_id, t.user_id), + }), +); export const genLikeId = () => { - return `comment_${nanoid(12)}`; + return `like_${nanoid(12)}`; };