Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const PATCH = withWorkspace(
const {
name,
subject,
preview,
from,
status,
bodyJson,
Expand Down Expand Up @@ -121,6 +122,7 @@ export const PATCH = withWorkspace(
data: {
...(name && { name }),
...(subject && { subject }),
...(preview !== undefined && { preview }),
...(from && { from }),
...(status && { status }),
...(bodyJson && { bodyJson }),
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export async function POST(req: Request) {
},
campaign: {
type: campaign.type,
subject: campaign.subject,
preview: campaign.preview,
body: renderCampaignEmailHTML({
content: campaign.bodyJson as unknown as TiptapNode,
variables: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,17 @@ import {
TooltipContent,
useKeyboardShortcut,
} from "@dub/ui";
import { capitalize } from "@dub/utils";
import { capitalize, cn } from "@dub/utils";
import { motion } from "motion/react";
import { useAction } from "next-safe-action/hooks";
import Link from "next/link";
import { useCallback, useEffect, useRef } from "react";
import {
PropsWithChildren,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { toast } from "sonner";
import { useDebouncedCallback } from "use-debounce";
Expand All @@ -38,7 +45,7 @@ import { TransactionalCampaignLogic } from "./transactional-campaign-logic";
import { isValidTriggerCondition } from "./utils";

const inputClassName =
"hover:border-border-subtle h-8 w-full rounded-md transition-colors duration-150 focus:border-black/75 border focus:ring-black/75 border-transparent px-1.5 py-0 text-sm text-content-default placeholder:text-content-muted hover:bg-neutral-100 hover:cursor-pointer";
"hover:border-border-subtle h-8 w-full rounded-md transition-colors duration-150 focus:border-black/75 border focus:ring-black/75 border-transparent px-1.5 py-0 sm:text-sm text-content-default placeholder:text-content-muted hover:bg-neutral-100 hover:cursor-pointer";

const labelClassName = "text-sm font-medium text-content-subtle";

Expand Down Expand Up @@ -95,6 +102,7 @@ export function CampaignEditor({ campaign }: { campaign: Campaign }) {
defaultValues: {
name: campaign.name,
subject: campaign.subject,
preview: campaign.preview,
from: campaign.from ?? undefined,
bodyJson: campaign.bodyJson,
groupIds: campaign.groups.map(({ id }) => id),
Expand All @@ -112,6 +120,26 @@ export function CampaignEditor({ campaign }: { campaign: Campaign }) {
formState: { dirtyFields },
} = form;

const previewInputRef = useRef<HTMLInputElement>(null);

const [showPreviewText, setShowPreviewText] = useState(
Boolean(campaign.preview),
);

// Show preview text when preview is set
useEffect(() => {
const { unsubscribe } = watch(({ preview }) => {
if (preview) setShowPreviewText(true);
});
return () => unsubscribe();
}, [watch]);

// Focus preview input when opened
useEffect(() => {
if (showPreviewText && !getValues("preview"))
previewInputRef.current?.focus();
}, [showPreviewText, getValues]);

const saveCampaign = useCallback(
async ({ isDraft = false }: { isDraft?: boolean }) => {
const allFormData = getValues();
Expand Down Expand Up @@ -215,6 +243,11 @@ export function CampaignEditor({ campaign }: { campaign: Campaign }) {
const statusBadge = CAMPAIGN_STATUS_BADGES[campaign.status];

const editorRef = useRef<{ setContent: (content: any) => void }>(null);
const previewInputProps = register("preview", {
onBlur: (e) => {
if (!e.target.value) setShowPreviewText(false);
},
});

return (
<FormProvider {...form}>
Expand Down Expand Up @@ -275,7 +308,7 @@ export function CampaignEditor({ campaign }: { campaign: Campaign }) {
contentWrapperClassName="flex flex-col"
>
<PageWidthWrapper className="mb-8 max-w-[600px]">
<div className="grid grid-cols-[max-content_minmax(0,1fr)] items-center gap-x-6 gap-y-2">
<div className="grid grid-cols-[max-content_minmax(0,1fr)] items-center gap-x-6 [&>*:nth-child(n+2)]:mt-2">
<span className={labelClassName}>Name</span>
<DisabledInputWrapper
tooltip={isReadOnly ? statusMessages[campaign.status] : ""}
Expand Down Expand Up @@ -326,7 +359,7 @@ export function CampaignEditor({ campaign }: { campaign: Campaign }) {
<input
type="text"
placeholder="Address"
className="text-content-default placeholder:text-content-muted min-w-0 flex-1 border-0 bg-transparent p-0 text-sm focus:outline-none focus:ring-0"
className="text-content-default placeholder:text-content-muted min-w-0 flex-1 border-0 bg-transparent p-0 focus:outline-none focus:ring-0 sm:text-sm"
disabled={isDisabled}
value={localPart}
onChange={(e) => {
Expand Down Expand Up @@ -402,14 +435,54 @@ export function CampaignEditor({ campaign }: { campaign: Campaign }) {
disabled={isReadOnly}
hideIcon={true}
>
<div className="relative">
<input
type="text"
placeholder="Enter a subject..."
className={cn(
inputClassName,
!isReadOnly && !showPreviewText && "pr-24",
)}
disabled={isReadOnly}
{...register("subject")}
/>
{!isReadOnly && (
<div className="absolute right-0 top-1/2 -translate-y-1/2">
<button
type="button"
onClick={() => setShowPreviewText(true)}
className={cn(
"text-content-subtle hover:text-content-default px-2 py-1 text-sm font-medium",
"transition-[transform,opacity] duration-150 ease-out",
showPreviewText && "translate-y-1 opacity-0",
)}
inert={showPreviewText}
>
Preview text
</button>
</div>
)}
</div>
</DisabledInputWrapper>

<ConditionalColumn show={showPreviewText}>
<span className={cn(labelClassName, "flex h-8 items-center")}>
Preview
</span>
</ConditionalColumn>
<ConditionalColumn show={showPreviewText}>
<input
type="text"
placeholder="Enter a subject..."
placeholder="Enter preview text..."
className={inputClassName}
disabled={isReadOnly}
{...register("subject")}
{...previewInputProps}
ref={(e) => {
previewInputProps.ref(e);
previewInputRef.current = e;
}}
/>
</DisabledInputWrapper>
</ConditionalColumn>

{campaign.type === "transactional" && (
<>
Expand Down Expand Up @@ -515,3 +588,23 @@ export function CampaignEditor({ campaign }: { campaign: Campaign }) {
</FormProvider>
);
}

const ConditionalColumn = ({
show,
children,
}: PropsWithChildren<{ show: boolean }>) => {
return (
<motion.div
initial={false}
animate={{ height: show ? "auto" : 0 }}
transition={{ duration: 0.15, ease: "easeOut" }}
className={cn(
"transition-[margin,opacity] duration-150",
!show && "!mt-0 opacity-0",
)}
inert={!show}
>
{children}
</motion.div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ function SendEmailPreviewModal({
const { control } = useCampaignFormContext();
const [emailAddresses, setEmailAddresses] = useState(user?.email ?? "");

const [subject, bodyJson, from] = useWatch({
const [subject, preview, bodyJson, from] = useWatch({
control,
name: ["subject", "bodyJson", "from"],
name: ["subject", "preview", "bodyJson", "from"],
});

const { executeAsync: sendEmailPreview, isPending } = useAction(
Expand Down Expand Up @@ -81,6 +81,7 @@ function SendEmailPreviewModal({
workspaceId,
campaignId,
subject,
preview,
bodyJson,
from,
emailAddresses: emails,
Expand Down
6 changes: 4 additions & 2 deletions apps/web/lib/actions/campaigns/send-campaign-preview-email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const sendPreviewEmailSchema = z
campaignId: z.string(),
workspaceId: z.string(),
subject: z.string().min(1, "Email subject is required."),
preview: z.string().nullish(),
from: z.string().email().optional(),
emailAddresses: z
.array(z.string().email())
Expand All @@ -32,7 +33,8 @@ export const sendCampaignPreviewEmail = authActionClient
.schema(sendPreviewEmailSchema)
.action(async ({ parsedInput, ctx }) => {
const { workspace } = ctx;
const { campaignId, subject, from, bodyJson, emailAddresses } = parsedInput;
const { campaignId, subject, preview, from, bodyJson, emailAddresses } =
parsedInput;

const programId = getDefaultProgramIdOrThrow(workspace);

Expand Down Expand Up @@ -66,7 +68,7 @@ export const sendCampaignPreviewEmail = authActionClient
},
campaign: {
type: campaign.type,
subject,
preview,
body: renderCampaignEmailHTML({
content: bodyJson as unknown as TiptapNode,
variables: {
Expand Down
1 change: 1 addition & 0 deletions apps/web/lib/api/campaigns/validate-campaign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export async function validateCampaign({
if (
input.name ||
input.subject ||
input.preview ||
input.bodyJson ||
input.groupIds ||
input.triggerCondition ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export const executeSendCampaignWorkflow = async ({
},
campaign: {
type: campaign.type,
subject: campaign.subject,
preview: campaign.preview,
body: renderCampaignEmailHTML({
content: campaign.bodyJson as unknown as TiptapNode,
variables: {
Expand Down
8 changes: 6 additions & 2 deletions apps/web/lib/api/workflows/render-campaign-email-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ export function renderCampaignEmailHTML({
levels: [1, 2],
},
}),
Image,
Image.configure({
HTMLAttributes: {
style: "max-width: 100%; height: auto; margin: 12px auto;",
},
}),
Mention.extend({
renderHTML({ node }: { node: any }) {
return [
Expand Down Expand Up @@ -84,7 +88,7 @@ const sanitizeHtmlBody = (body: string) => {
],
allowedAttributes: {
a: ["href", "name", "target", "rel"],
img: ["src", "alt", "title"],
img: ["src", "alt", "title", "style"],
ul: ["style"],
ol: ["style"],
li: ["style"],
Expand Down
2 changes: 2 additions & 0 deletions apps/web/lib/zod/schemas/campaigns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const CampaignSchema = z.object({
id: z.string(),
name: z.string(),
subject: z.string(),
preview: z.string().nullable().default(null),
from: z.string().nullable(),
bodyJson: z.record(z.string(), z.any()),
type: z.nativeEnum(CampaignType),
Expand Down Expand Up @@ -84,6 +85,7 @@ export const updateCampaignSchema = z
.string()
.trim()
.max(100, "Subject must be less than 100 characters."),
preview: z.string().nullish(),
from: z.string().email().trim().toLowerCase(),
bodyJson: z.record(z.string(), z.any()),
triggerCondition: workflowConditionSchema.nullish(),
Expand Down
9 changes: 7 additions & 2 deletions apps/web/scripts/perplexity/ban-partners.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { syncTotalCommissions } from "@/lib/api/partners/sync-total-commissions";
import { BAN_PARTNER_REASONS } from "@/lib/zod/schemas/partners";
import PartnerBanned from "@dub/email/templates/partner-banned";
import { prisma } from "@dub/prisma";
import "dotenv-flow/config";
import * as fs from "fs";
import * as Papa from "papaparse";
import { linkCache } from "../../lib/api/links/cache";
import { syncTotalCommissions } from "../../lib/api/partners/sync-total-commissions";
import { queueBatchEmail } from "../../lib/email/queue-batch-email";

let partnersToBan: string[] = [];
Expand Down Expand Up @@ -38,12 +38,15 @@ async function main() {
partnerId: {
in: partnersToBan,
},
status: {
not: "banned",
},
},
include: {
links: true,
partner: true,
},
take: 100,
take: 200,
});

if (programEnrollments.length === 0) {
Expand Down Expand Up @@ -127,6 +130,8 @@ async function main() {
),
);

console.log("commissionsRes", commissionsRes);

const qstashRes = await queueBatchEmail<typeof PartnerBanned>(
programEnrollments
.filter((p) => p.partner.email)
Expand Down
1 change: 1 addition & 0 deletions apps/web/tests/campaigns/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const expectedCampaign: Partial<Campaign> = {
...campaign,
type: "transactional",
status: expect.any(String),
preview: null,
from: null,
scheduledAt: null,
groups: [{ id: E2E_PARTNER_GROUP.id }],
Expand Down
Loading