-
Notifications
You must be signed in to change notification settings - Fork 0
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
573bafb
commit 4b084eb
Showing
11 changed files
with
652 additions
and
0 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
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}/> | ||
</> | ||
) | ||
} |
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,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} /> | ||
</> | ||
) | ||
} |
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,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} /> | ||
</> | ||
) | ||
} | ||
|
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,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> | ||
</> | ||
) | ||
} |
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,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> | ||
) | ||
} |
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,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
60
src/components/forms/freeze-entries/actions/save-freeze-entry.ts
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,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"} | ||
|
||
} |
Oops, something went wrong.