-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1412 from dubinc/utm-templates
ENG-570: UTM templates
- Loading branch information
Showing
41 changed files
with
1,596 additions
and
242 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,89 @@ | ||
import { DubApiError } from "@/lib/api/errors"; | ||
import { withWorkspace } from "@/lib/auth"; | ||
import { prisma } from "@/lib/prisma"; | ||
import { updateUTMTemplateBodySchema } from "@/lib/zod/schemas/utm"; | ||
import { NextResponse } from "next/server"; | ||
|
||
// PATCH /api/utm/[id] – update a UTM template | ||
export const PATCH = withWorkspace( | ||
async ({ req, params, workspace }) => { | ||
const { id } = params; | ||
const props = updateUTMTemplateBodySchema.parse(await req.json()); | ||
|
||
const template = await prisma.utmTemplate.findFirst({ | ||
where: { | ||
id, | ||
projectId: workspace.id, | ||
}, | ||
}); | ||
|
||
if (!template) { | ||
throw new DubApiError({ | ||
code: "not_found", | ||
message: "Template not found.", | ||
}); | ||
} | ||
|
||
try { | ||
const response = await prisma.utmTemplate.update({ | ||
where: { | ||
id, | ||
projectId: workspace.id, | ||
}, | ||
data: { | ||
...props, | ||
}, | ||
}); | ||
|
||
return NextResponse.json(response); | ||
} catch (error) { | ||
if (error.code === "P2002") { | ||
throw new DubApiError({ | ||
code: "conflict", | ||
message: "A template with that name already exists.", | ||
}); | ||
} | ||
|
||
throw error; | ||
} | ||
}, | ||
{ | ||
requiredPermissions: ["links.write"], | ||
}, | ||
); | ||
|
||
// DELETE /api/utm/[id] – delete a UTM template for a workspace | ||
export const DELETE = withWorkspace( | ||
async ({ params, workspace }) => { | ||
const { id } = params; | ||
try { | ||
const response = await prisma.utmTemplate.delete({ | ||
where: { | ||
id, | ||
projectId: workspace.id, | ||
}, | ||
}); | ||
|
||
if (!response) { | ||
throw new DubApiError({ | ||
code: "not_found", | ||
message: "UTM template not found.", | ||
}); | ||
} | ||
|
||
return NextResponse.json({ id }); | ||
} catch (error) { | ||
if (error.code === "P2025") { | ||
throw new DubApiError({ | ||
code: "not_found", | ||
message: "UTM template not found.", | ||
}); | ||
} | ||
|
||
throw error; | ||
} | ||
}, | ||
{ | ||
requiredPermissions: ["links.write"], | ||
}, | ||
); |
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,65 @@ | ||
import { DubApiError } from "@/lib/api/errors"; | ||
import { withWorkspace } from "@/lib/auth"; | ||
import { prisma } from "@/lib/prisma"; | ||
import { createUTMTemplateBodySchema } from "@/lib/zod/schemas/utm"; | ||
import { NextResponse } from "next/server"; | ||
|
||
// GET /api/utm - get all UTM templates for a workspace | ||
export const GET = withWorkspace( | ||
async ({ workspace, headers }) => { | ||
const templates = await prisma.utmTemplate.findMany({ | ||
where: { | ||
projectId: workspace.id, | ||
}, | ||
orderBy: { | ||
updatedAt: "desc", | ||
}, | ||
include: { | ||
user: true, | ||
}, | ||
take: 50, | ||
}); | ||
|
||
return NextResponse.json(templates, { headers }); | ||
}, | ||
{ | ||
requiredPermissions: ["links.read"], | ||
}, | ||
); | ||
|
||
// POST /api/utm - create a new UTM template for a workspace | ||
export const POST = withWorkspace( | ||
async ({ req, workspace, session, headers }) => { | ||
const props = createUTMTemplateBodySchema.parse(await req.json()); | ||
|
||
const existingTemplate = await prisma.utmTemplate.findFirst({ | ||
where: { | ||
projectId: workspace.id, | ||
name: props.name, | ||
}, | ||
}); | ||
|
||
if (existingTemplate) { | ||
throw new DubApiError({ | ||
code: "conflict", | ||
message: "A template with that name already exists.", | ||
}); | ||
} | ||
|
||
const response = await prisma.utmTemplate.create({ | ||
data: { | ||
projectId: workspace.id, | ||
userId: session?.user.id, | ||
...props, | ||
}, | ||
}); | ||
|
||
return NextResponse.json(response, { | ||
headers, | ||
status: 201, | ||
}); | ||
}, | ||
{ | ||
requiredPermissions: ["links.write"], | ||
}, | ||
); |
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
31 changes: 31 additions & 0 deletions
31
apps/web/app/app.dub.co/(dashboard)/[slug]/settings/library/header.tsx
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,31 @@ | ||
"use client"; | ||
|
||
import useWorkspace from "@/lib/swr/use-workspace"; | ||
import { TabSelect } from "@dub/ui"; | ||
import { useRouter, useSelectedLayoutSegment } from "next/navigation"; | ||
|
||
export default function LibraryHeader() { | ||
const router = useRouter(); | ||
const { slug } = useWorkspace(); | ||
|
||
const selectedLayoutSegment = useSelectedLayoutSegment(); | ||
const page = selectedLayoutSegment === null ? "" : selectedLayoutSegment; | ||
|
||
return ( | ||
<div className="border-b border-gray-200"> | ||
<h1 className="text-2xl font-semibold tracking-tight text-black"> | ||
Library | ||
</h1> | ||
<TabSelect | ||
options={[ | ||
{ id: "tags", label: "Tags" }, | ||
{ id: "utm", label: "UTM Templates" }, | ||
]} | ||
selected={page} | ||
onSelect={(id) => { | ||
router.push(`/${slug}/settings/library/${id}`); | ||
}} | ||
/> | ||
</div> | ||
); | ||
} |
11 changes: 11 additions & 0 deletions
11
apps/web/app/app.dub.co/(dashboard)/[slug]/settings/library/layout.tsx
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,11 @@ | ||
import { ReactNode } from "react"; | ||
import LibraryHeader from "./header"; | ||
|
||
export default function LibraryLayout({ children }: { children: ReactNode }) { | ||
return ( | ||
<div className="grid gap-4"> | ||
<LibraryHeader /> | ||
{children} | ||
</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
File renamed without changes.
File renamed without changes.
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
75 changes: 75 additions & 0 deletions
75
apps/web/app/app.dub.co/(dashboard)/[slug]/settings/library/utm/page-client.tsx
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,75 @@ | ||
"use client"; | ||
|
||
import useWorkspace from "@/lib/swr/use-workspace"; | ||
import { UtmTemplateWithUserProps } from "@/lib/types"; | ||
import { useAddEditUtmTemplateModal } from "@/ui/modals/add-edit-utm-template.modal"; | ||
import EmptyState from "@/ui/shared/empty-state"; | ||
import { CardList } from "@dub/ui"; | ||
import { DiamondTurnRight } from "@dub/ui/src"; | ||
import { fetcher } from "@dub/utils"; | ||
import { Dispatch, SetStateAction, createContext, useState } from "react"; | ||
import useSWR from "swr"; | ||
import { TemplateCard } from "./template-card"; | ||
import { TemplateCardPlaceholder } from "./template-card-placeholder"; | ||
|
||
export const TemplatesListContext = createContext<{ | ||
openMenuTemplateId: string | null; | ||
setOpenMenuTemplateId: Dispatch<SetStateAction<string | null>>; | ||
}>({ | ||
openMenuTemplateId: null, | ||
setOpenMenuTemplateId: () => {}, | ||
}); | ||
|
||
export default function WorkspaceUtmTemplatesClient() { | ||
const { id: workspaceId } = useWorkspace(); | ||
|
||
const { data: templates, isLoading } = useSWR<UtmTemplateWithUserProps[]>( | ||
workspaceId && `/api/utm?workspaceId=${workspaceId}`, | ||
fetcher, | ||
{ | ||
dedupingInterval: 60000, | ||
}, | ||
); | ||
|
||
const [openMenuTemplateId, setOpenMenuTemplateId] = useState<string | null>( | ||
null, | ||
); | ||
|
||
const { AddEditUtmTemplateModal, AddUtmTemplateButton } = | ||
useAddEditUtmTemplateModal(); | ||
|
||
return ( | ||
<> | ||
<div className="grid gap-4"> | ||
<div className="flex justify-end gap-6"> | ||
<AddUtmTemplateButton /> | ||
</div> | ||
{workspaceId && <AddEditUtmTemplateModal />} | ||
|
||
{isLoading || templates?.length ? ( | ||
<TemplatesListContext.Provider | ||
value={{ openMenuTemplateId, setOpenMenuTemplateId }} | ||
> | ||
<CardList variant="compact" loading={isLoading}> | ||
{templates?.length | ||
? templates.map((template) => ( | ||
<TemplateCard key={template.id} template={template} /> | ||
)) | ||
: Array.from({ length: 6 }).map((_, idx) => ( | ||
<TemplateCardPlaceholder key={idx} /> | ||
))} | ||
</CardList> | ||
</TemplatesListContext.Provider> | ||
) : ( | ||
<div className="flex flex-col items-center gap-4 rounded-xl border border-gray-200 py-10"> | ||
<EmptyState | ||
icon={DiamondTurnRight} | ||
title="No templates found for this workspace" | ||
/> | ||
<AddUtmTemplateButton /> | ||
</div> | ||
)} | ||
</div> | ||
</> | ||
); | ||
} |
10 changes: 10 additions & 0 deletions
10
apps/web/app/app.dub.co/(dashboard)/[slug]/settings/library/utm/page.tsx
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,10 @@ | ||
import { Suspense } from "react"; | ||
import WorkspaceUtmTemplatesClient from "./page-client"; | ||
|
||
export default function WorkspaceUtmTemplates() { | ||
return ( | ||
<Suspense> | ||
<WorkspaceUtmTemplatesClient /> | ||
</Suspense> | ||
); | ||
} |
19 changes: 19 additions & 0 deletions
19
.../web/app/app.dub.co/(dashboard)/[slug]/settings/library/utm/template-card-placeholder.tsx
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,19 @@ | ||
import { CardList } from "@dub/ui"; | ||
|
||
export function TemplateCardPlaceholder() { | ||
return ( | ||
<CardList.Card> | ||
<div className="flex h-8 items-center justify-between gap-5 sm:gap-8 md:gap-12"> | ||
<div className="flex items-center gap-3"> | ||
<div className="h-5 w-5 animate-pulse rounded-md bg-gray-200" /> | ||
<div className="h-5 w-16 animate-pulse rounded-md bg-gray-200 sm:w-32" /> | ||
</div> | ||
<div className="flex items-center gap-5 sm:gap-8 md:gap-12"> | ||
<div className="h-5 w-12 animate-pulse rounded-md bg-gray-200" /> | ||
<div className="hidden h-5 w-16 animate-pulse rounded-md bg-gray-200 sm:block" /> | ||
<div className="w-8" /> | ||
</div> | ||
</div> | ||
</CardList.Card> | ||
); | ||
} |
Oops, something went wrong.