Skip to content

Commit

Permalink
feat: migrate stores
Browse files Browse the repository at this point in the history
  • Loading branch information
sadmann7 committed Mar 3, 2024
1 parent 9bb92cd commit 0f15f93
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 214 deletions.
16 changes: 13 additions & 3 deletions src/app/(lobby)/_components/lobby.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { productCategories } from "@/config/products"
import { siteConfig } from "@/config/site"
import { type getGithubStars } from "@/lib/actions/github"
import { type getFeaturedProducts } from "@/lib/actions/product"
import { type getFeaturedStores } from "@/lib/fetchers/store"
import { type getFeaturedStores } from "@/lib/actions/store"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { ProductCard } from "@/components/cards/product-card"
Expand Down Expand Up @@ -36,16 +36,26 @@ export async function Lobby({
return (
<Shell className="max-w-6xl">
<section className="mx-auto flex w-full max-w-5xl flex-col items-center justify-center gap-4 py-24 text-center md:py-32">
<Link href={siteConfig.links.x} target="_blank" rel="noreferrer">
<Badge
aria-hidden="true"
className="rounded-full px-3.5 py-1.5"
variant="secondary"
>
Rewritting with Next.js 14 mental models, follow along on X for
updates
</Badge>
<span className="sr-only">X</span>
</Link>
<Link href={siteConfig.links.github} target="_blank" rel="noreferrer">
<Badge
aria-hidden="true"
className="rounded-md px-3.5 py-1.5"
className="rounded-full px-3.5 py-1.5"
variant="secondary"
>
<Icons.gitHub className="mr-2 size-3.5" aria-hidden="true" />
{githubStars} stars on GitHub
</Badge>

<span className="sr-only">GitHub</span>
</Link>
<h1 className="text-balance font-heading text-3xl sm:text-5xl md:text-6xl lg:text-7xl">
Expand Down
2 changes: 1 addition & 1 deletion src/app/(lobby)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from "react"

import { getGithubStars } from "@/lib/actions/github"
import { getFeaturedProducts } from "@/lib/actions/product"
import { getFeaturedStores } from "@/lib/fetchers/store"
import { getFeaturedStores } from "@/lib/actions/store"

import { Lobby } from "./_components/lobby"
import { LobbySkeleton } from "./_components/lobby-skeleton"
Expand Down
233 changes: 201 additions & 32 deletions src/lib/actions/store.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,203 @@
"use server"

import { revalidatePath, revalidateTag } from "next/cache"
import {
unstable_cache as cache,
unstable_noStore as noStore,
revalidatePath,
revalidateTag,
} from "next/cache"
import { redirect } from "next/navigation"
import { db } from "@/db"
import { stores } from "@/db/schema"
import { and, eq, not } from "drizzle-orm"
import { products } from "drizzle/schema"
import { z } from "zod"
import { products, stores, type Store } from "@/db/schema"
import type { SearchParams } from "@/types"
import { and, asc, desc, eq, isNull, not, sql } from "drizzle-orm"
import { type z } from "zod"

import { getErrorMessage } from "@/lib/handle-error"
import { slugify } from "@/lib/utils"
import { storeSchema, updateStoreSchema } from "@/lib/validations/store"
import {
getStoresSchema,
updateStoreSchema,
type addStoreSchema,
} from "@/lib/validations/store"

const extendedStoreSchema = storeSchema.extend({
userId: z.string(),
})
export async function getFeaturedStores() {
return await cache(
async () => {
return db
.select({
id: stores.id,
name: stores.name,
description: stores.description,
stripeAccountId: stores.stripeAccountId,
})
.from(stores)
.limit(4)
.leftJoin(products, eq(products.storeId, stores.id))
.groupBy(stores.id)
.orderBy(desc(stores.active), desc(sql<number>`count(*)`))
},
["featured-stores"],
{
revalidate: 1,
tags: ["featured-stores"],
}
)()
}

export async function getUserStores(input: { userId: string }) {
return await cache(
async () => {
return db
.select({
id: stores.id,
name: stores.name,
description: stores.description,
stripeAccountId: stores.stripeAccountId,
})
.from(stores)
.leftJoin(products, eq(products.storeId, stores.id))
.groupBy(stores.id)
.orderBy(desc(stores.stripeAccountId), desc(sql<number>`count(*)`))
.where(eq(stores.userId, input.userId))
},
["user-stores"],
{
revalidate: 900,
tags: ["user-stores"],
}
)()
}

export async function getStores(input: SearchParams) {
noStore()
try {
const search = getStoresSchema.parse(input)

const limit = search.per_page
const offset = (search.page - 1) * limit
const [column, order] =
(search.sort?.split(".") as [
keyof Store | undefined,
"asc" | "desc" | undefined,
]) ?? []
const statuses = search.statuses?.split(".") ?? []

const { data, count } = await db.transaction(async (tx) => {
const data = await tx
.select({
id: stores.id,
name: stores.name,
description: stores.description,
stripeAccountId: stores.stripeAccountId,
productCount: sql<number>`count(*)`,
})
.from(stores)
.limit(limit)
.offset(offset)
.leftJoin(products, eq(stores.id, products.storeId))
.where(
and(
search.user_id ? eq(stores.userId, search.user_id) : undefined,
statuses.includes("active") && !statuses.includes("inactive")
? not(isNull(stores.stripeAccountId))
: undefined,
statuses.includes("inactive") && !statuses.includes("active")
? isNull(stores.stripeAccountId)
: undefined
)
)
.groupBy(stores.id)
.orderBy(
input.sort === "stripeAccountId.asc"
? asc(stores.stripeAccountId)
: input.sort === "stripeAccountId.desc"
? desc(stores.stripeAccountId)
: input.sort === "productCount.asc"
? asc(sql<number>`count(*)`)
: input.sort === "productCount.desc"
? desc(sql<number>`count(*)`)
: column && column in stores
? order === "asc"
? asc(stores[column])
: desc(stores[column])
: desc(stores.createdAt)
)

const count = await tx
.select({
count: sql<number>`count(*)`,
})
.from(stores)
.where(
and(
search.user_id ? eq(stores.userId, search.user_id) : undefined,
statuses.includes("active") && !statuses.includes("inactive")
? not(isNull(stores.stripeAccountId))
: undefined,
statuses.includes("inactive") && !statuses.includes("active")
? isNull(stores.stripeAccountId)
: undefined
)
)
.execute()
.then((res) => res[0]?.count ?? 0)

export async function addStore(rawInput: z.infer<typeof extendedStoreSchema>) {
const input = extendedStoreSchema.parse(rawInput)
return {
data,
count,
}
})

const storeWithSameName = await db.query.stores.findFirst({
where: eq(stores.name, input.name),
})
const pageCount = Math.ceil(count / limit)

if (storeWithSameName) {
throw new Error("Store name already taken.")
return {
data,
pageCount,
}
} catch (err) {
console.error(err)
return {
data: [],
pageCount: 0,
}
}
}

export async function addStore(
input: z.infer<typeof addStoreSchema> & { userId: string }
) {
try {
const storeWithSameName = await db.query.stores.findFirst({
where: eq(stores.name, input.name),
})

if (storeWithSameName) {
throw new Error("Store name already taken.")
}

await db.insert(stores).values({
name: input.name,
description: input.description,
userId: input.userId,
slug: slugify(input.name),
})

await db.insert(stores).values({
name: input.name,
description: input.description,
userId: input.userId,
slug: slugify(input.name),
})
revalidateTag("user-stores")

revalidateTag("user-stores")
return {
data: null,
error: null,
}
} catch (err) {
return {
data: null,
error: getErrorMessage(err),
}
}
}

export async function updateStore(storeId: number, fd: FormData) {
export async function updateStore(storeId: string, fd: FormData) {
try {
const input = updateStoreSchema.parse({
name: fd.get("name"),
Expand Down Expand Up @@ -66,16 +227,18 @@ export async function updateStore(storeId: number, fd: FormData) {
revalidatePath(`/dashboard/stores/${storeId}`)

return {
message: "Store updated successfully.",
data: null,
error: null,
}
} catch (err) {
throw err instanceof Error
? err
: new Error("Something went wrong, please try again.")
return {
data: null,
error: getErrorMessage(err),
}
}
}

export async function deleteStore(storeId: number) {
export async function deleteStore(storeId: string) {
try {
const store = await db.query.stores.findFirst({
where: eq(stores.id, storeId),
Expand All @@ -96,9 +259,15 @@ export async function deleteStore(storeId: number) {
const path = "/dashboard/stores"
revalidatePath(path)
redirect(path)

return {
data: null,
error: null,
}
} catch (err) {
throw err instanceof Error
? err
: new Error("Something went wrong, please try again.")
return {
data: null,
error: getErrorMessage(err),
}
}
}
Loading

0 comments on commit 0f15f93

Please sign in to comment.