Skip to content

Commit

Permalink
feat: add freeze entries
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelblijleven committed Oct 22, 2023
1 parent 573bafb commit 4b084eb
Show file tree
Hide file tree
Showing 11 changed files with 652 additions and 0 deletions.
43 changes: 43 additions & 0 deletions src/app/coffee/freeze/[entryId]/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {CoffeeForm} from "@/app/coffee/components/coffee-form";
import {Title} from "@/components/layout/title";
import {User} from "@clerk/nextjs/api";
import {currentUser} from "@clerk/nextjs";
import {getBeanDetails, getCoffeeIdsForUsers} from "@/lib/db/beans/get-bean-details";
import {notFound} from "next/navigation";
import {Inputs} from "@/app/coffee/actions/coffee-form/form-schema";
import {undefined} from "zod";
import {getFreezeEntry} from "@/lib/db/freeze-entries/get-freeze-entry";
import {FreezeEntryInput} from "@/components/forms/freeze-entries/schema";
import {FreezeEntryForm} from "@/components/forms/freeze-entries/form";

export default async function EditFreezeEntryPage({ params }: { params: { entryId: string } }) {
const user: User | null = await currentUser();

if (!user) return notFound();

const entry = await getFreezeEntry(params.entryId, user.publicMetadata.databaseId as number)

if (!entry) return notFound();

const values: FreezeEntryInput = {
publicId: entry.publicId,
label: entry.label ?? "",
beanPublicId: entry.beanPublicId as string,
weight: entry.weight ?? "",
freezeDate: entry.freezeDate ?? undefined,
frozen: entry.frozen,
notes: entry.notes ?? ""
}

const beans = await getCoffeeIdsForUsers(user.publicMetadata.databaseId as number);

return (
<>
<Title
title={"Freeze entry"}
subtitle={"Edit this entry to your frozen backlog 🥶"}
/>
<FreezeEntryForm values={values} beans={beans}/>
</>
)
}
67 changes: 67 additions & 0 deletions src/app/coffee/freeze/[entryId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {Metadata, ResolvingMetadata} from "next";
import {getFreezeEntry} from "@/lib/db/freeze-entries/get-freeze-entry";
import {User} from "@clerk/nextjs/api";
import {currentUser} from "@clerk/nextjs";
import {getBeanDetails} from "@/lib/db/beans/get-bean-details";
import {canView} from "@/lib/perms";
import {notFound} from "next/navigation";
import {Title} from "@/components/layout/title";
import {BeanDetail} from "@/app/coffee/[coffeeId]/components/bean-detail";
import Link from "next/link";
import {buttonVariants} from "@/components/ui/button";
import {FreezeEntryDetail} from "@/components/detail-pages/freeze-entry-detail";

type PageProps = {
params: { coffeeId: string}
}

export async function generateMetadata({params}: PageProps, parent: ResolvingMetadata,): Promise<Metadata> {
// read route params
const id = params.coffeeId

// fetch data
const entry = await getFreezeEntry(id, undefined)

if (!entry || !entry.isPublic) {
const title = (await parent).title
return {
title: title ?? "Coffee",
description: "Keep track of your coffee backlog"
}
}

return {
title: entry.beanName,
description: `Freeze entry`
}
}

function Buttons({user, entry} :{user: User | null, entry: Awaited<ReturnType<typeof getFreezeEntry>>}) {
if (!entry || !user || user.publicMetadata.databaseId !== entry.userId) return null;

return (
<div className={"flex flex-row-reverse gap-2"}>
<Link href={`/coffee/freeze/${entry.publicId}/edit`} className={buttonVariants({size: "sm", variant: "outline"})}>
Edit
</Link>
</div>
);
}

export default async function FreezeEntryDetailPage({ params }: { params: { entryId: string } }) {
const user: User | null = await currentUser();
const entry = await getFreezeEntry(params.entryId, user?.publicMetadata?.databaseId as number || undefined)

if (!entry || !canView(user, entry)) return notFound();

return (
<>
<Title
title={`Freeze entry for ${entry.beanName}`}
subtitle={`Frozen at ${entry.freezeDate}`}
/>
<Buttons user={user} entry={entry} />
<FreezeEntryDetail entry={entry} />
</>
)
}
35 changes: 35 additions & 0 deletions src/app/coffee/freeze/add/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {FreezeEntryForm} from "@/components/forms/freeze-entries/form";
import {User} from "@clerk/nextjs/api";
import {currentUser} from "@clerk/nextjs";
import {notFound, useSearchParams} from "next/navigation";
import {getCoffeeIdsForUsers} from "@/lib/db/beans/get-bean-details";
import {Title} from "@/components/layout/title";

export default async function AddFreezeEntryPage({searchParams}: {searchParams: Record<string, string | undefined> }) {
const user: User | null = await currentUser();

if (!user) return notFound();

const beanPublicID = searchParams.id;
const beanName = searchParams.bean
const forBean = !!beanPublicID && !!beanName;

let beans;

if (forBean) {
beans = [{publicId: beanPublicID, name: beanName}];
} else {
beans = await getCoffeeIdsForUsers(user.publicMetadata.databaseId as number);
}

return (
<>
<Title
title={"Freeze entry"}
subtitle={"Add an entry to your frozen backlog 🥶"}
/>
<FreezeEntryForm beans={beans} forBean={forBean} />
</>
)
}

62 changes: 62 additions & 0 deletions src/app/coffee/freeze/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {db} from "@/db";
import {and, eq} from "drizzle-orm";
import {freezeEntries} from "@/db/schema";
import {type FreezeEntry, FreezeEntryDataTable, columns} from "@/components/overview-pages/freeze-entry-datatable";
import {type User} from "@clerk/nextjs/api";
import {currentUser} from "@clerk/nextjs";
import {notFound} from "next/navigation";
import {Title} from "@/components/layout/title";
import Link from "next/link";
import {cn} from "@/lib/utils";
import {buttonVariants} from "@/components/ui/button";

async function getFreezeEntries(userId: number, page: number, defrosted: boolean): Promise<FreezeEntry[]> {
const pageSize = 10;

return await db.query.freezeEntries.findMany({
where: and(eq(freezeEntries.userId, userId), eq(freezeEntries.frozen, !defrosted)),
with: {
bean: true,
},
orderBy: (freezeEntries, {desc}) => [desc(freezeEntries.created), desc(freezeEntries.id)],
limit: pageSize + 1,
offset: (page - 1) * pageSize
})
}

export default async function FreezeEntriesPage({searchParams}: {searchParams: Record<string, string | string[] | undefined>}) {
const user: User | null = await currentUser();

if (!user) return notFound();

const page = parseInt(searchParams?.page as string ?? "1");
const defrosted = parseInt(searchParams?.defrosted as string ?? "0");
const userId = user.publicMetadata.databaseId as number;
const data = await getFreezeEntries(userId, page, !!defrosted);

return (
<>
<Title title={"Freeze entries"} subtitle={"Your frozen coffee backlog"} />
<section>
<div className={"justify-between items-center space-x-2"}>
<Link href={"/coffee/freeze/add"} className={cn(buttonVariants({variant: "default", size: "default"}), "hidden md:inline-flex")}>Add entry</Link>
<Link href={"/coffee/freeze/add"} className={cn(buttonVariants({variant: "default", size: "sm"}), "inline-flex md:hidden")}>Add entry</Link>
</div>
</section>
<section>
<div
aria-hidden='true'
className='pointer-events-none absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80 opacity-20'>
<div
style={{
clipPath:
'polygon(17% 12%, 8% 11%, 0% 11%, 0% 20%, 0% 33%, 11% 35%, 17% 42%, 19% 50%, 12% 53%, 12% 62%, 10% 74%, 4% 78%, 4% 83%, 4% 99%, 18% 99%, 30% 95%, 31% 81%, 48% 73%, 63% 72%, 66% 80%, 67% 87%, 74% 93%, 83% 97%, 95% 96%, 99% 87%, 94% 74%, 86% 57%, 71% 42%, 78% 26%, 86% 15%, 95% 8%, 98% 4%, 94% 1%, 77% 0%, 67% 8%, 60% 22%, 49% 19%, 30% 18%)',
}}
className='relative left-[calc(50%-11rem)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 rotate-[30deg] bg-gradient-to-tr from-primary to-[#fc6b03] opacity-30 sm:left-[calc(50%-30rem)] sm:w-[72.1875rem]'
/>
</div>
<FreezeEntryDataTable columns={columns} data={data} />
</section>
</>
)
}
8 changes: 8 additions & 0 deletions src/components/detail-pages/detail-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function DetailItem({label, value}: { label: string, value: string }) {
return (
<div className={"flex flex-col gap-y-1"}>
<span className={"text-sm text-muted-foreground font-semibold"}>{label}</span>
<span className={""}>{value}</span>
</div>
)
}
32 changes: 32 additions & 0 deletions src/components/detail-pages/freeze-entry-detail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {getFreezeEntry} from "@/lib/db/freeze-entries/get-freeze-entry";
import {DetailItem} from "@/components/detail-pages/detail-item";

type Details = Awaited<ReturnType<typeof getFreezeEntry>>

type PageProps = {
entry: Details;
}


export function FreezeEntryDetail({entry}: PageProps) {
if (!entry) return null;


return (
<>
<section className={"space-y-2"}>
<h3 className={"font-bold text-lg"}>Details</h3>
<div className={"grid grid-cols-2 gap-2 max-w-md"}>
<DetailItem label={"Label"} value={entry.label ?? "-"}/>
<DetailItem label={"Freeze date"} value={entry.freezeDate ?? "-"}/>
<DetailItem label={"Freeze amount"} value={entry.weight ?? "-"}/>
<DetailItem label={"Is frozen"} value={entry.frozen ? "Yes" : "No"}/>
</div>
</section>
<section>
<h3 className={"font-bold text-lg"}>Notes</h3>
<p className={"min-h-[300px] w-full md:w-1/2 rounded-lg outline outline-accent"}>{entry.notes || ""}</p>
</section>
</>
)
}
60 changes: 60 additions & 0 deletions src/components/forms/freeze-entries/actions/save-freeze-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use server"

import {type FreezeEntryInput, freezeEntryInsertSchema} from "@/components/forms/freeze-entries/schema";
import {db} from "@/db";
import {type User} from "@clerk/nextjs/api";
import {currentUser} from "@clerk/nextjs";
import {and, eq} from "drizzle-orm";
import {beans, freezeEntries} from "@/db/schema";
import {createInsertSchema} from "drizzle-zod";
import {type z} from "zod";
import {revalidatePath} from "next/cache";

const freezeEntrySchema = createInsertSchema(freezeEntries);

export async function saveFreezeEntry(data: Partial<FreezeEntryInput>) {
const user: User | null = await currentUser();

if (!user) return {success: false, publicId: data.publicId ?? null, detail: "not authenticated"};
if (!data.beanPublicId) return {success: false, publicId: null, detail: "no bean public id provided"};

const bean = await db.query.beans.findFirst({
where: and(
eq(beans.publicId, data.beanPublicId),
eq(beans.userId, user.publicMetadata.databaseId as number)
)
});

if (!bean) return {success: false, publicId: data.publicId ?? null, detail: "bean not found"}

const beanId = bean.id;

delete data["beanPublicId"] // delete this here, otherwise drizzle will throw an error when inserting/updating
const values = {...data, beanId: beanId, userId: user.publicMetadata.databaseId} as z.infer<typeof freezeEntrySchema>

if (!!data.publicId) {
// Update existing entry
await db
.update(freezeEntries)
.set(values)
.where(
and(
eq(freezeEntries.publicId, data.publicId),
eq(freezeEntries.userId, user.publicMetadata.databaseId as number)
)
);

// Revalidate path since we've modified the content
revalidatePath(`/coffee/freeze/${data.publicId}`, "page")
return {success: true, publicId: data.publicId, detail: "successfully updated entry"}
}

// New entry
const result = await db.insert(freezeEntries).values(values);
const [dbData] = await db.select({publicId: freezeEntries.publicId}).from(freezeEntries).where(
eq(freezeEntries.id, parseInt(result.insertId))
)

return {success: true, publicId: dbData.publicId, detail: "successfully created entry"}

}
Loading

0 comments on commit 4b084eb

Please sign in to comment.