Skip to content

Commit

Permalink
Transfer links feature
Browse files Browse the repository at this point in the history
Closes dubinc#176
  • Loading branch information
steven-tey committed Feb 12, 2024
1 parent 77c2d07 commit ee2e74a
Show file tree
Hide file tree
Showing 35 changed files with 715 additions and 1,349 deletions.
6 changes: 3 additions & 3 deletions apps/web/app/api/cron/links/event/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export async function POST(req: Request) {

const { linkId, type } = body as {
linkId: string;
type: "create" | "edit";
type: "create" | "edit" | "transfer";
};

const link = await prisma.link.findUnique({
Expand All @@ -39,8 +39,8 @@ export async function POST(req: Request) {
return new Response("Link not found", { status: 200 });
}

// if the link is a dub.sh link, do some checks
if (link.domain === "dub.sh") {
// if the link is a dub.sh link (and is not a transfer event), do some checks
if (link.domain === "dub.sh" && type !== "transfer") {
const invalidFavicon = await fetch(
`${GOOGLE_FAVICON_URL}${getApexDomain(link.url)}`,
).then((res) => !res.ok);
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/api/links/[linkId]/archive/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { NextResponse } from "next/server";

// POST /api/links/[linkId]/archive – archive a link
export const POST = withAuth(async ({ headers, link }) => {
const response = await archiveLink(link!.domain, link!.key, true);
const response = await archiveLink({ linkId: link!.id, archived: true });
return NextResponse.json(response, { headers });
});

// DELETE /api/links/[linkId]/archive – unarchive a link
export const DELETE = withAuth(async ({ headers, link }) => {
const response = await archiveLink(link!.domain, link!.key, false);
const response = await archiveLink({ linkId: link!.id, archived: false });
return NextResponse.json(response, { headers });
});
66 changes: 66 additions & 0 deletions apps/web/app/api/links/[linkId]/transfer/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { transferLink } from "@/lib/api/links";
import { withAuth } from "@/lib/auth";
import { qstash } from "@/lib/cron";
import prisma from "@/lib/prisma";
import { APP_DOMAIN_WITH_NGROK } from "@dub/utils";
import { NextResponse } from "next/server";

// POST /api/links/[linkId]/transfer – transfer a link to another project
export const POST = withAuth(async ({ req, headers, session, link }) => {
let body: { newProjectId: string };

try {
body = await req.json();
} catch (error) {
return new Response("Missing or invalid body.", { status: 400, headers });
}

if (!body.newProjectId) {
return new Response("Missing new project ID.", { status: 400, headers });
}

const newProject = await prisma.project.findUnique({
where: { id: body.newProjectId },
select: {
linksUsage: true,
linksLimit: true,
users: {
where: {
userId: session.user.id,
},
select: {
role: true,
},
},
},
});

if (!newProject || newProject.users.length === 0) {
return new Response("New project not found.", { status: 404, headers });
}

if (newProject.linksUsage >= newProject.linksLimit) {
return new Response("New project has reached its link limit.", {
status: 403,
headers,
});
}

const response = await Promise.all([
transferLink({
linkId: link!.id,
newProjectId: body.newProjectId,
}),
qstash.publishJSON({
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/links/event`,
body: {
linkId: link!.id,
type: "transfer",
},
}),
]);

return NextResponse.json(response, {
headers,
});
});
34 changes: 25 additions & 9 deletions apps/web/lib/api/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -677,20 +677,36 @@ export async function deleteLink(link: LinkProps) {
]);
}

export async function archiveLink(
domain: string,
key: string,
archived = true,
) {
export async function archiveLink({
linkId,
archived,
}: {
linkId: string;
archived: boolean;
}) {
return await prisma.link.update({
where: {
domain_key: {
domain,
key,
},
id: linkId,
},
data: {
archived,
},
});
}

export async function transferLink({
linkId,
newProjectId,
}: {
linkId: string;
newProjectId: string;
}) {
return await prisma.link.update({
where: {
id: linkId,
},
data: {
projectId: newProjectId,
},
});
}
7 changes: 3 additions & 4 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@
"vaul": "^0.6.8"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.0.0",
"@types/dotenv-flow": "^3.3.2",
"@types/he": "^1.2.3",
"@types/html-escaper": "^3.0.0",
Expand All @@ -103,9 +102,9 @@
"papaparse": "^5.4.1",
"postcss": "^8.4.31",
"postcss-import": "^15.1.0",
"prettier": "^3.0.3",
"prettier-plugin-organize-imports": "^3.2.3",
"prettier-plugin-tailwindcss": "^0.1.13",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.3.3",
"tsx": "^3.14.0",
"turbo": "^1.10.14",
Expand Down
26 changes: 25 additions & 1 deletion apps/web/ui/links/link-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
CopyPlus,
Edit3,
EyeOff,
FolderInput,
Mail,
MessageCircle,
QrCode,
Expand All @@ -49,6 +50,7 @@ import punycode from "punycode/";
import { useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import useSWR, { mutate } from "swr";
import { useTransferLinkModal } from "../modals/transfer-link-modal";

export default function LinkCard({
props,
Expand Down Expand Up @@ -133,6 +135,9 @@ export default function LinkCard({
props,
archived: !archived,
});
const { setShowTransferLinkModal, TransferLinkModal } = useTransferLinkModal({
props,
});
const { setShowDeleteLinkModal, DeleteLinkModal } = useDeleteLinkModal({
props,
});
Expand Down Expand Up @@ -181,7 +186,7 @@ export default function LinkCard({
// - there is no existing modal backdrop
if (
(selected || openPopover) &&
["e", "d", "q", "a", "x"].includes(e.key)
["e", "d", "q", "a", "t", "x"].includes(e.key)
) {
setSelected(false);
e.preventDefault();
Expand All @@ -198,6 +203,9 @@ export default function LinkCard({
case "a":
setShowArchiveLinkModal(true);
break;
case "t":
setShowTransferLinkModal(true);
break;
case "x":
setShowDeleteLinkModal(true);
break;
Expand Down Expand Up @@ -225,6 +233,7 @@ export default function LinkCard({
<AddEditLinkModal />
<DuplicateLinkModal />
<ArchiveLinkModal />
<TransferLinkModal />
<DeleteLinkModal />
</>
)}
Expand Down Expand Up @@ -470,6 +479,21 @@ export default function LinkCard({
A
</kbd>
</button>
<button
onClick={() => {
setOpenPopover(false);
setShowTransferLinkModal(true);
}}
className="group flex w-full items-center justify-between rounded-md p-2 text-left text-sm font-medium text-gray-500 transition-all duration-75 hover:bg-gray-100"
>
<IconMenu
text="Transfer"
icon={<FolderInput className="h-4 w-4" />}
/>
<kbd className="hidden rounded bg-gray-100 px-2 py-0.5 text-xs font-light text-gray-500 transition-all duration-75 group-hover:bg-gray-200 sm:inline-block">
T
</kbd>
</button>
<button
onClick={() => {
setOpenPopover(false);
Expand Down
4 changes: 2 additions & 2 deletions apps/web/ui/modals/accept-invite-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function AcceptInviteModal({
project on {process.env.NEXT_PUBLIC_APP_NAME}
</p>
</div>
<div className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:px-16">
<div className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:rounded-b-2xl sm:px-16">
<button
onClick={() => {
setAccepting(true);
Expand Down Expand Up @@ -84,7 +84,7 @@ function AcceptInviteModal({
This invite has expired or is no longer valid.
</p>
</div>
<div className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:px-16">
<div className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:rounded-b-2xl sm:px-16">
<Link
href="/"
className="flex h-10 w-full items-center justify-center rounded-md border border-black bg-black text-sm text-white transition-all hover:bg-white hover:text-black focus:outline-none"
Expand Down
2 changes: 1 addition & 1 deletion apps/web/ui/modals/add-edit-domain-modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ function AddEditDomainModal({
setSaving(false);
});
}}
className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:px-16"
className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:rounded-b-2xl sm:px-16"
>
<div>
<div className="flex items-center justify-between">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/ui/modals/add-edit-link-modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ function AddEditLinkModal({
<div
className={`${
atBottom ? "" : "md:shadow-[0_-20px_30px_-10px_rgba(0,0,0,0.1)]"
} z-10 bg-gray-50 px-4 py-8 transition-all md:sticky md:bottom-0 md:px-16`}
} z-10 bg-gray-50 px-4 py-8 transition-all sm:rounded-b-2xl md:sticky md:bottom-0 md:px-16`}
>
{homepageDemo ? (
<Button
Expand Down
2 changes: 1 addition & 1 deletion apps/web/ui/modals/add-project-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ function AddProjectModalHelper({
setSaving(false);
});
}}
className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:px-16"
className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:rounded-b-2xl sm:px-16"
>
<div>
<label htmlFor="name" className="flex items-center space-x-2">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/ui/modals/archive-link-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ function ArchiveLinkModal({
</p>
</div>

<div className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:px-16">
<div className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:rounded-b-2xl sm:px-16">
<Button
onClick={handleArchiveRequest}
autoFocus
Expand Down
2 changes: 1 addition & 1 deletion apps/web/ui/modals/complete-setup-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function CompleteSetupModal({
links.
</p>
</div>
<div className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:px-12">
<div className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:rounded-b-2xl sm:px-12">
<div className="grid divide-y divide-gray-200 rounded-lg border border-gray-200 bg-white">
{tasks.map(({ display, cta, checked }) => {
const contents = (
Expand Down
2 changes: 1 addition & 1 deletion apps/web/ui/modals/delete-account-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function DeleteAccountModal({
error: (err) => err,
});
}}
className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:px-16"
className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:rounded-b-2xl sm:px-16"
>
<div>
<label htmlFor="verification" className="block text-sm text-gray-700">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/ui/modals/delete-link-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function DeleteLinkModal({
setDeleting(false);
});
}}
className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:px-16"
className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:rounded-b-2xl sm:px-16"
>
<div>
<label htmlFor="verification" className="block text-sm text-gray-700">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/ui/modals/delete-token-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function DeleteTokenModal({
</p>
</div>

<div className="flex flex-col space-y-4 bg-gray-50 px-4 py-8 text-left sm:px-16">
<div className="flex flex-col space-y-4 bg-gray-50 px-4 py-8 text-left sm:rounded-b-2xl sm:px-16">
<div className="relative flex items-center space-x-3 rounded-md border border-gray-300 bg-white px-1 py-3">
<Badge variant="neutral" className="absolute right-2 top-2">
{token.partialKey}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/ui/modals/edit-role-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function EditRoleModal({
</p>
</div>

<div className="flex flex-col space-y-4 bg-gray-50 px-4 py-8 text-left sm:px-16">
<div className="flex flex-col space-y-4 bg-gray-50 px-4 py-8 text-left sm:rounded-b-2xl sm:px-16">
<div className="flex items-center space-x-3 rounded-md border border-gray-300 bg-white p-3">
<Avatar user={user} />
<div className="flex flex-col">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/ui/modals/google-oauth-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function GoogleOauthModal({
</a>
</p>
</div>
<div className="flex flex-col space-y-3 bg-gray-50 px-4 py-8 text-left sm:px-16">
<div className="flex flex-col space-y-3 bg-gray-50 px-4 py-8 text-left sm:rounded-b-2xl sm:px-16">
<Button
text="Connect Google Account"
onClick={() => {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/ui/modals/import-bitly-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function ImportBitlyModal({
</p>
</div>

<div className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:px-16">
<div className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:rounded-b-2xl sm:px-16">
{isLoading ? (
<button className="flex flex-col items-center justify-center space-y-4 bg-none">
<LoadingSpinner />
Expand Down
2 changes: 1 addition & 1 deletion apps/web/ui/modals/import-rebrandly-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ function ImportRebrandlyModal({
</p>
</div>

<div className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:px-16">
<div className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:rounded-b-2xl sm:px-16">
{isLoading ? (
<button className="flex flex-col items-center justify-center space-y-4 bg-none">
<LoadingSpinner />
Expand Down
2 changes: 1 addition & 1 deletion apps/web/ui/modals/import-short-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ function ImportShortModal({
</p>
</div>

<div className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:px-16">
<div className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:rounded-b-2xl sm:px-16">
{isLoading ? (
<button className="flex flex-col items-center justify-center space-y-4 bg-none">
<LoadingSpinner />
Expand Down
2 changes: 1 addition & 1 deletion apps/web/ui/modals/invite-teammate-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ function InviteTeammateModal({
setInviting(false);
});
}}
className="flex flex-col space-y-4 bg-gray-50 px-4 py-8 text-left sm:px-16"
className="flex flex-col space-y-4 bg-gray-50 px-4 py-8 text-left sm:rounded-b-2xl sm:px-16"
>
<div>
<label htmlFor="email" className="block text-sm text-gray-700">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/ui/modals/remove-saml-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function RemoveSAMLModal({
</p>
</div>

<div className="flex flex-col space-y-4 bg-gray-50 px-4 py-8 text-left sm:px-16">
<div className="flex flex-col space-y-4 bg-gray-50 px-4 py-8 text-left sm:rounded-b-2xl sm:px-16">
<div className="flex items-center space-x-3 rounded-md border border-gray-300 bg-white p-3">
<img
src={SAML_PROVIDERS.find((p) => p.name === provider)!.logo}
Expand Down
Loading

0 comments on commit ee2e74a

Please sign in to comment.