Skip to content

Commit

Permalink
Merge branch 'main' into extensionanirban
Browse files Browse the repository at this point in the history
  • Loading branch information
anirbanpaulcom authored Jun 4, 2024
2 parents d5802bf + fce3713 commit e9b8fab
Show file tree
Hide file tree
Showing 11 changed files with 345 additions and 127 deletions.
65 changes: 65 additions & 0 deletions apps/web/app/admin.dub.co/components/send-thanks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use client";

import { LoadingSpinner } from "@dub/ui";
import { cn } from "@dub/utils";
import { useFormStatus } from "react-dom";
import { toast } from "sonner";

export default function SendThanks() {
return (
<div className="flex flex-col space-y-5">
<form
action={async (formData) => {
if (
!confirm(
`This will send an email to ${formData.get("email")} with a thank you message. Are you sure?`,
)
) {
return;
}
await fetch("/api/admin/send-thanks", {
method: "POST",
body: JSON.stringify({
email: formData.get("email"),
}),
}).then(async (res) => {
if (res.ok) {
toast.success("Successfully sent email");
} else {
const error = await res.text();
toast.error(error);
}
});
}}
>
<Form />
</form>
</div>
);
}

const Form = () => {
const { pending } = useFormStatus();

return (
<div className="relative flex w-full rounded-md shadow-sm">
<input
name="email"
id="email"
type="email"
required
disabled={pending}
autoComplete="off"
className={cn(
"block w-full rounded-md border-gray-300 text-sm text-gray-900 placeholder-gray-400 focus:border-gray-500 focus:outline-none focus:ring-gray-500",
pending && "bg-gray-100",
)}
placeholder="stey@vercel.com"
aria-invalid="true"
/>
{pending && (
<LoadingSpinner className="absolute inset-y-0 right-2 my-auto h-full w-5 text-gray-400" />
)}
</div>
);
};
10 changes: 8 additions & 2 deletions apps/web/app/admin.dub.co/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { constructMetadata } from "@dub/utils";
import ImpersonateUser from "./components/impersonate-user";
import ImpersonateWorkspace from "./components/impersonate-workspace";
import RefreshDomain from "./components/refresh-domain";
import SendThanks from "./components/send-thanks";

export const metadata = constructMetadata({
title: "Dub Admin",
Expand All @@ -13,13 +14,13 @@ export default function AdminPage() {
<div className="mx-auto flex w-full max-w-screen-sm flex-col divide-y divide-gray-200 overflow-auto bg-white">
<div className="flex flex-col space-y-4 px-5 py-10">
<h2 className="text-xl font-semibold">Impersonate User</h2>
<p className="text-sm text-gray-500">Get a login link for a user.</p>
<p className="text-sm text-gray-500">Get a login link for a user</p>
<ImpersonateUser />
</div>
<div className="flex flex-col space-y-4 px-5 py-10">
<h2 className="text-xl font-semibold">Impersonate Workspace</h2>
<p className="text-sm text-gray-500">
Get a login link for the owner of a workspace.
Get a login link for the owner of a workspace
</p>
<ImpersonateWorkspace />
</div>
Expand All @@ -30,6 +31,11 @@ export default function AdminPage() {
</p>
<RefreshDomain />
</div>
<div className="flex flex-col space-y-4 px-5 py-10">
<h2 className="text-xl font-semibold">Send Thanks</h2>
<p className="text-sm text-gray-500">Send thank you email to a user</p>
<SendThanks />
</div>
</div>
);
}
23 changes: 23 additions & 0 deletions apps/web/app/api/admin/send-thanks/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { withAdmin } from "@/lib/auth";
import { sendEmail } from "emails";
import UpgradeEmail from "emails/upgrade-email";
import { NextResponse } from "next/server";

// POST /api/admin/send-thanks
export const POST = withAdmin(async ({ req }) => {
const { email } = await req.json();

await sendEmail({
email,
subject: "Thank you for upgrading to Dub.co Pro!",
react: UpgradeEmail({
name: null,
email,
plan: "pro",
}),
marketing: true,
bcc: process.env.TRUSTPILOT_BCC_EMAIL,
});

return NextResponse.json({ success: true });
});
3 changes: 3 additions & 0 deletions apps/web/emails/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ export const sendEmail = async ({
email,
subject,
from,
bcc,
text,
react,
marketing,
}: {
email: string;
subject: string;
from?: string;
bcc?: string;
text?: string;
react?: ReactElement<any, string | JSXElementConstructor<any>>;
marketing?: boolean;
Expand Down Expand Up @@ -44,6 +46,7 @@ export const sendEmail = async ({
? "system@dub.co"
: `${process.env.NEXT_PUBLIC_APP_NAME} <system@${process.env.NEXT_PUBLIC_APP_DOMAIN}>`,
To: email,
Bcc: bcc,
ReplyTo: process.env.NEXT_PUBLIC_IS_DUB
? "support@dub.co"
: `support@${process.env.NEXT_PUBLIC_APP_DOMAIN}`,
Expand Down
76 changes: 71 additions & 5 deletions apps/web/lib/zod/schemas/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,26 +92,92 @@ export const analyticsQuerySchema = z.object({
.optional()
.describe("The country to retrieve analytics for.")
.openapi({ ref: "countryCode" }),
city: z.string().optional().describe("The city to retrieve analytics for."),
city: z
.string()
.optional()
.describe("The city to retrieve analytics for.")
.openapi({
examples: [
"New York",
"Los Angeles",
"Chicago",
"Houston",
"Phoenix",
"Philadelphia",
"San Antonio",
"San Diego",
"Dallas",
"Tokyo",
"Delhi",
"Shanghai",
"São Paulo",
"Mumbai",
"Beijing",
],
}),
device: z
.string()
.optional()
.transform((v) => capitalize(v) as string | undefined)
.describe("The device to retrieve analytics for."),
.describe("The device to retrieve analytics for.")
.openapi({
examples: ["Desktop", "Mobile", "Tablet", "Wearable", "Smarttv"],
}),
browser: z
.string()
.optional()
.transform((v) => capitalize(v) as string | undefined)
.describe("The browser to retrieve analytics for."),
.describe("The browser to retrieve analytics for.")
.openapi({
examples: [
"Chrome",
"Mobile Safari",
"Edge",
"Instagram",
"Firefox",
"Facebook",
"WebKit",
"Samsung Browser",
"Chrome WebView",
"Safari",
"Opera",
"IE",
"Yandex",
],
}),
os: z
.string()
.optional()
.transform((v) => capitalize(v) as string | undefined)
.describe("The OS to retrieve analytics for."),
.describe("The OS to retrieve analytics for.")
.openapi({
examples: [
"Windows",
"iOS",
"Android",
"Mac OS",
"Linux",
"Ubuntu",
"Chromium OS",
"Fedora",
],
}),
referer: z
.string()
.optional()
.describe("The referer to retrieve analytics for."),
.describe("The referer to retrieve analytics for.")
.openapi({
examples: [
"(direct)",
"t.co",
"youtube.com",
"perplexity.ai",
"l.instagram.com",
"m.facebook.com",
"linkedin.com",
"google.com",
],
}),
url: z.string().optional().describe("The URL to retrieve analytics for."),
tagId: z
.string()
Expand Down
20 changes: 20 additions & 0 deletions apps/web/ui/analytics/referer-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { cn } from "@dub/utils";
import { Link2 } from "lucide-react";
import LinkLogo from "../links/link-logo";

export default function RefererIcon({
display,
className,
}: {
display: string;
className?: string;
}) {
return display === "(direct)" ? (
<Link2 className={cn("h-4 w-4", className)} />
) : (
<LinkLogo
apexDomain={display}
className={cn("h-4 w-4 sm:h-4 sm:w-4", className)}
/>
);
}
89 changes: 2 additions & 87 deletions apps/web/ui/analytics/referer.tsx
Original file line number Diff line number Diff line change
@@ -1,96 +1,11 @@
import { BlurImage, Modal, useRouterStuff } from "@dub/ui";
import { BlurImage, useRouterStuff } from "@dub/ui";
import { GOOGLE_FAVICON_URL } from "@dub/utils";
import { Link2, Maximize } from "lucide-react";
import { useState } from "react";
import { Link2 } from "lucide-react";
import { AnalyticsCard } from "./analytics-card";
import { AnalyticsLoadingSpinner } from "./analytics-loading-spinner";
import BarList from "./bar-list";
import { useAnalyticsFilterOption } from "./utils";

function RefererOld() {
const data = useAnalyticsFilterOption("referers");

const { queryParams } = useRouterStuff();
const [showModal, setShowModal] = useState(false);

const barList = (limit?: number) => (
<BarList
tab="Referrer"
data={
data?.map((d) => ({
icon:
d.referer === "(direct)" ? (
<Link2 className="h-4 w-4" />
) : (
<BlurImage
src={`${GOOGLE_FAVICON_URL}${d.referer}`}
alt={d.referer}
width={20}
height={20}
className="h-4 w-4 rounded-full"
/>
),
title: d.referer,
href: queryParams({
set: {
referer: d.referer,
},
getNewPath: true,
}) as string,
value: d.count || 0,
})) || []
}
maxValue={(data && data[0]?.count) || 0}
barBackground="bg-red-100"
hoverBackground="bg-red-100/50"
setShowModal={setShowModal}
{...(limit && { limit })}
/>
);

return (
<>
<Modal
showModal={showModal}
setShowModal={setShowModal}
className="max-w-lg"
>
<div className="border-b border-gray-200 px-6 py-4">
<h1 className="text-lg font-semibold">Referers</h1>
</div>
{barList()}
</Modal>
<div className="scrollbar-hide relative z-0 h-[400px] border border-gray-200 bg-white px-7 py-5 sm:rounded-xl">
<div className="mb-3 flex justify-between">
<h1 className="text-lg font-semibold">Referers</h1>
</div>
{data ? (
data.length > 0 ? (
barList(9)
) : (
<div className="flex h-[300px] items-center justify-center">
<p className="text-sm text-gray-600">No data available</p>
</div>
)
) : (
<div className="flex h-[300px] items-center justify-center">
<AnalyticsLoadingSpinner />
</div>
)}
{data && data.length > 9 && (
<button
onClick={() => setShowModal(true)}
className="absolute inset-x-0 bottom-4 z-10 mx-auto flex w-full items-center justify-center space-x-2 rounded-md bg-gradient-to-b from-transparent to-white py-2 text-gray-500 transition-all hover:text-gray-800 active:scale-95"
>
<Maximize className="h-4 w-4" />
<p className="text-xs font-semibold uppercase">View all</p>
</button>
)}
</div>
</>
);
}

export default function Referer() {
const { queryParams } = useRouterStuff();

Expand Down
Loading

0 comments on commit e9b8fab

Please sign in to comment.