rebrand to useSend (#210)
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
|
||||
import { Plus } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
|
||||
import { Edit } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
@@ -20,13 +20,13 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { api } from "~/trpc/react";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
import { SesSetting } from "@prisma/client";
|
||||
|
||||
const FormSchema = z.object({
|
||||
|
@@ -2,12 +2,13 @@
|
||||
|
||||
import AddSesConfiguration from "./add-ses-configuration";
|
||||
import SesConfigurations from "./ses-configurations";
|
||||
import { H1 } from "@usesend/ui";
|
||||
|
||||
export default function ApiKeysPage() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="font-bold text-lg">Admin</h1>
|
||||
<H1>Admin</H1>
|
||||
<AddSesConfiguration />
|
||||
</div>
|
||||
<div className="mt-10">
|
||||
|
@@ -7,12 +7,12 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@unsend/ui/src/table";
|
||||
} from "@usesend/ui/src/table";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { api } from "~/trpc/react";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
import EditSesConfiguration from "./edit-ses-configuration";
|
||||
import { TextWithCopyButton } from "@unsend/ui/src/text-with-copy";
|
||||
import { TextWithCopyButton } from "@usesend/ui/src/text-with-copy";
|
||||
|
||||
export default function SesConfigurations() {
|
||||
const sesSettingsQuery = api.admin.getSesSettings.useQuery();
|
||||
|
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { api } from "~/trpc/react";
|
||||
import { Spinner } from "@unsend/ui/src/spinner";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Editor } from "@unsend/email-editor";
|
||||
import { Spinner } from "@usesend/ui/src/spinner";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import { Editor } from "@usesend/email-editor";
|
||||
import { use, useState } from "react";
|
||||
import { Campaign } from "@prisma/client";
|
||||
import {
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "@unsend/ui/src/select";
|
||||
} from "@usesend/ui/src/select";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -31,8 +31,8 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
} from "@usesend/ui/src/form";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import {
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@unsend/ui/src/accordion";
|
||||
} from "@usesend/ui/src/accordion";
|
||||
|
||||
const sendSchema = z.object({
|
||||
confirmation: z.string(),
|
||||
@@ -63,7 +63,7 @@ export default function EditCampaignPage({
|
||||
{ campaignId },
|
||||
{
|
||||
enabled: !!campaignId,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
@@ -98,7 +98,7 @@ function CampaignEditor({
|
||||
const utils = api.useUtils();
|
||||
|
||||
const [json, setJson] = useState<Record<string, any> | undefined>(
|
||||
campaign.content ? JSON.parse(campaign.content) : undefined
|
||||
campaign.content ? JSON.parse(campaign.content) : undefined,
|
||||
);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [name, setName] = useState(campaign.name);
|
||||
@@ -106,10 +106,10 @@ function CampaignEditor({
|
||||
const [from, setFrom] = useState(campaign.from);
|
||||
const [contactBookId, setContactBookId] = useState(campaign.contactBookId);
|
||||
const [replyTo, setReplyTo] = useState<string | undefined>(
|
||||
campaign.replyTo[0]
|
||||
campaign.replyTo[0],
|
||||
);
|
||||
const [previewText, setPreviewText] = useState<string | null>(
|
||||
campaign.previewText
|
||||
campaign.previewText,
|
||||
);
|
||||
const [openSendDialog, setOpenSendDialog] = useState(false);
|
||||
|
||||
@@ -135,7 +135,7 @@ function CampaignEditor({
|
||||
|
||||
const deboucedUpdateCampaign = useDebouncedCallback(
|
||||
updateEditorContent,
|
||||
1000
|
||||
1000,
|
||||
);
|
||||
|
||||
async function onSendCampaign(values: z.infer<typeof sendSchema>) {
|
||||
@@ -160,14 +160,14 @@ function CampaignEditor({
|
||||
onError: (error) => {
|
||||
toast.error(`Failed to send campaign: ${error.message}`);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const handleFileChange = async (file: File) => {
|
||||
if (file.size > IMAGE_SIZE_LIMIT) {
|
||||
throw new Error(
|
||||
`File should be less than ${IMAGE_SIZE_LIMIT / 1024 / 1024}MB`
|
||||
`File should be less than ${IMAGE_SIZE_LIMIT / 1024 / 1024}MB`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ function CampaignEditor({
|
||||
const confirmation = sendForm.watch("confirmation");
|
||||
|
||||
const contactBook = contactBooksQuery.data?.find(
|
||||
(book) => book.id === contactBookId
|
||||
(book) => book.id === contactBookId,
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -220,7 +220,7 @@ function CampaignEditor({
|
||||
toast.error(`${e.message}. Reverting changes.`);
|
||||
setName(campaign.name);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -315,7 +315,7 @@ function CampaignEditor({
|
||||
toast.error(`${e.message}. Reverting changes.`);
|
||||
setSubject(campaign.subject);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}}
|
||||
className="mt-1 py-1 text-sm block w-full outline-none border-b border-transparent focus:border-border bg-transparent"
|
||||
@@ -350,7 +350,7 @@ function CampaignEditor({
|
||||
toast.error(`${e.message}. Reverting changes.`);
|
||||
setFrom(campaign.from);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -381,7 +381,7 @@ function CampaignEditor({
|
||||
toast.error(`${e.message}. Reverting changes.`);
|
||||
setReplyTo(campaign.replyTo[0]);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -414,7 +414,7 @@ function CampaignEditor({
|
||||
toast.error(`${e.message}. Reverting changes.`);
|
||||
setPreviewText(campaign.previewText ?? "");
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}}
|
||||
className="mt-1 py-1 text-sm block w-full outline-none border-b border-transparent bg-transparent focus:border-border"
|
||||
@@ -440,7 +440,7 @@ function CampaignEditor({
|
||||
onError: () => {
|
||||
setContactBookId(campaign.contactBookId);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
setContactBookId(val);
|
||||
}}
|
||||
|
@@ -7,10 +7,11 @@ import {
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@unsend/ui/src/breadcrumb";
|
||||
} from "@usesend/ui/src/breadcrumb";
|
||||
import Link from "next/link";
|
||||
import { H2 } from "@usesend/ui";
|
||||
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
import { api } from "~/trpc/react";
|
||||
import { use } from "react";
|
||||
|
||||
@@ -80,7 +81,7 @@ export default function CampaignDetailsPage({
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className="mt-10">
|
||||
<h2 className="text-xl font-semibold mb-4"> Statistics</h2>
|
||||
<H2 className="mb-4"> Statistics</H2>
|
||||
<div className="flex gap-4">
|
||||
{statusCards.map((card) => (
|
||||
<div
|
||||
@@ -110,7 +111,7 @@ export default function CampaignDetailsPage({
|
||||
|
||||
{campaign.html && (
|
||||
<div className=" rounded-lg mt-16">
|
||||
<h2 className="text-xl font-semibold mb-4">Email</h2>
|
||||
<H2 className="mb-4">Email</H2>
|
||||
|
||||
<div className="p-2 rounded-lg border shadow flex flex-col gap-4 w-full">
|
||||
<div className="flex flex-col gap-3 px-4 py-1">
|
||||
|
@@ -7,11 +7,11 @@ import {
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableCell,
|
||||
} from "@unsend/ui/src/table";
|
||||
} from "@usesend/ui/src/table";
|
||||
import { api } from "~/trpc/react";
|
||||
import { useUrlState } from "~/hooks/useUrlState";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { CampaignStatus } from "@prisma/client";
|
||||
import DeleteCampaign from "./delete-campaign";
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
} from "@unsend/ui/src/select";
|
||||
} from "@usesend/ui/src/select";
|
||||
|
||||
export default function CampaignList() {
|
||||
const [page, setPage] = useUrlState("page", "1");
|
||||
|
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
|
||||
import { api } from "~/trpc/react";
|
||||
import { useState } from "react";
|
||||
@@ -24,9 +24,9 @@ import { Plus } from "lucide-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
|
||||
const campaignSchema = z.object({
|
||||
name: z.string({ required_error: "Name is required" }).min(1, {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import { api } from "~/trpc/react";
|
||||
import React, { useState } from "react";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
import { Campaign } from "@prisma/client";
|
||||
|
||||
const campaignSchema = z.object({
|
||||
|
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import { api } from "~/trpc/react";
|
||||
import React, { useState } from "react";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { Copy } from "lucide-react";
|
||||
import { Campaign } from "@prisma/client";
|
||||
|
||||
|
@@ -2,12 +2,13 @@
|
||||
|
||||
import CampaignList from "./campaign-list";
|
||||
import CreateCampaign from "./create-campaign";
|
||||
import { H1 } from "@usesend/ui";
|
||||
|
||||
export default function ContactsPage() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="font-bold text-lg">Campaigns</h1>
|
||||
<H1>Campaigns</H1>
|
||||
<CreateCampaign />
|
||||
</div>
|
||||
<CampaignList />
|
||||
|
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Textarea } from "@unsend/ui/src/textarea";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Textarea } from "@usesend/ui/src/textarea";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
|
||||
import { api } from "~/trpc/react";
|
||||
import { useState } from "react";
|
||||
@@ -26,7 +26,7 @@ import { useRouter } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
|
||||
const contactsSchema = z.object({
|
||||
contacts: z.string({ required_error: "Contacts are required" }).min(1, {
|
||||
|
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "@unsend/ui/src/select";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
} from "@usesend/ui/src/select";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@unsend/ui/src/table";
|
||||
} from "@usesend/ui/src/table";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import Image from "next/image";
|
||||
import { useUrlState } from "~/hooks/useUrlState";
|
||||
@@ -23,14 +23,14 @@ import { api } from "~/trpc/react";
|
||||
import { getGravatarUrl } from "~/utils/gravatar-utils";
|
||||
import DeleteContact from "./delete-contact";
|
||||
import EditContact from "./edit-contact";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@unsend/ui/src/tooltip";
|
||||
} from "@usesend/ui/src/tooltip";
|
||||
import { UnsubscribeReason } from "@prisma/client";
|
||||
|
||||
function getUnsubscribeReason(reason: UnsubscribeReason) {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import { api } from "~/trpc/react";
|
||||
import React, { useState } from "react";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
import { Contact } from "@prisma/client";
|
||||
|
||||
const contactSchema = z.object({
|
||||
|
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
|
||||
import { api } from "~/trpc/react";
|
||||
import { useState } from "react";
|
||||
@@ -26,8 +26,8 @@ import { useRouter } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { Switch } from "@unsend/ui/src/switch";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { Switch } from "@usesend/ui/src/switch";
|
||||
import { Contact } from "@prisma/client";
|
||||
|
||||
const contactSchema = z.object({
|
||||
|
@@ -8,20 +8,20 @@ import {
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@unsend/ui/src/breadcrumb";
|
||||
} from "@usesend/ui/src/breadcrumb";
|
||||
import Link from "next/link";
|
||||
import AddContact from "./add-contact";
|
||||
import ContactList from "./contact-list";
|
||||
import { TextWithCopyButton } from "@unsend/ui/src/text-with-copy";
|
||||
import { TextWithCopyButton } from "@usesend/ui/src/text-with-copy";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import EmojiPicker, { Theme } from "emoji-picker-react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@unsend/ui/src/popover";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { useTheme } from "@unsend/ui";
|
||||
} from "@usesend/ui/src/popover";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { useTheme } from "@usesend/ui";
|
||||
import { use } from "react";
|
||||
|
||||
export default function ContactsPage({
|
||||
@@ -51,7 +51,7 @@ export default function ContactsPage({
|
||||
...old,
|
||||
...data,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
onSettled: () => {
|
||||
|
@@ -1,19 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
|
||||
import { api } from "~/trpc/react";
|
||||
import { useState } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
import { useUpgradeModalStore } from "~/store/upgradeModalStore";
|
||||
import { LimitReason } from "~/lib/constants/plans";
|
||||
|
||||
|
@@ -8,7 +8,7 @@ import EditContactBook from "./edit-contact-book";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { motion } from "framer-motion";
|
||||
import { useUrlState } from "~/hooks/useUrlState";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
export default function ContactBooksList() {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import { api } from "~/trpc/react";
|
||||
import React, { useState } from "react";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
import { ContactBook } from "@prisma/client";
|
||||
|
||||
const contactBookSchema = z.object({
|
||||
|
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -16,14 +16,14 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
import { api } from "~/trpc/react";
|
||||
import { useState } from "react";
|
||||
import { Edit } from "lucide-react";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
|
||||
const contactBookSchema = z.object({
|
||||
name: z.string().min(1, { message: "Name is required" }),
|
||||
|
@@ -2,12 +2,13 @@
|
||||
|
||||
import AddContactBook from "./add-contact-book";
|
||||
import ContactBooksList from "./contact-books-list";
|
||||
import { H1 } from "@usesend/ui";
|
||||
|
||||
export default function ContactsPage() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="font-semibold text-xl">Contact books</h1>
|
||||
<H1>Contact books</H1>
|
||||
<AddContactBook />
|
||||
</div>
|
||||
<ContactBooksList />
|
||||
|
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { AppSidebar } from "~/components/AppSideBar";
|
||||
import { SidebarInset, SidebarTrigger } from "@unsend/ui/src/sidebar";
|
||||
import { SidebarProvider } from "@unsend/ui/src/sidebar";
|
||||
import { useIsMobile } from "@unsend/ui/src/hooks/use-mobile";
|
||||
import { SidebarInset, SidebarTrigger } from "@usesend/ui/src/sidebar";
|
||||
import { SidebarProvider } from "@usesend/ui/src/sidebar";
|
||||
import { useIsMobile } from "@usesend/ui/src/hooks/use-mobile";
|
||||
import { UpgradeModal } from "~/components/payments/UpgradeModal";
|
||||
|
||||
export function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import React from "react";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@unsend/ui/src/tabs";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@usesend/ui/src/tabs";
|
||||
import { useUrlState } from "~/hooks/useUrlState";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "@unsend/ui/src/select";
|
||||
} from "@usesend/ui/src/select";
|
||||
import { api } from "~/trpc/react";
|
||||
|
||||
interface DashboardFiltersProps {
|
||||
|
@@ -13,8 +13,8 @@ import {
|
||||
import { EmailStatusIcon } from "../emails/email-status-badge";
|
||||
import { EmailStatus } from "@prisma/client";
|
||||
import { api } from "~/trpc/react";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import { useTheme } from "@unsend/ui";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
import { useTheme } from "@usesend/ui";
|
||||
import { useColors } from "./hooks/useColors";
|
||||
|
||||
interface EmailChartProps {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useTheme } from "@unsend/ui";
|
||||
import { useTheme } from "@usesend/ui";
|
||||
|
||||
export function useColors() {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
import EmailChart from "./email-chart";
|
||||
import DashboardFilters from "./dashboard-filters";
|
||||
import { H1 } from "@usesend/ui";
|
||||
import { useUrlState } from "~/hooks/useUrlState";
|
||||
import { ReputationMetrics } from "./reputation-metrics";
|
||||
|
||||
@@ -13,7 +14,7 @@ export default function Dashboard() {
|
||||
<div>
|
||||
<div className="w-full">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="font-semibold text-xl">Analytics</h1>
|
||||
<H1>Analytics</H1>
|
||||
<DashboardFilters
|
||||
days={days ?? "7"}
|
||||
setDays={setDays}
|
||||
|
@@ -3,7 +3,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@unsend/ui/src/tooltip";
|
||||
} from "@usesend/ui/src/tooltip";
|
||||
import {
|
||||
CheckCircle2,
|
||||
CheckCircle2Icon,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
|
||||
import { api } from "~/trpc/react";
|
||||
import { useState } from "react";
|
||||
import { CheckIcon, ClipboardCopy, Eye, EyeOff, Plus } from "lucide-react";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
|
||||
const apiKeySchema = z.object({
|
||||
name: z.string({ required_error: "Name is required" }).min(1, {
|
||||
|
@@ -7,11 +7,11 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@unsend/ui/src/table";
|
||||
} from "@usesend/ui/src/table";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { api } from "~/trpc/react";
|
||||
import DeleteApiKey from "./delete-api-key";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
|
||||
export default function ApiList() {
|
||||
const apiKeysQuery = api.apiKey.getApiKeys.useQuery();
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -9,11 +9,11 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import { api } from "~/trpc/react";
|
||||
import React, { useState } from "react";
|
||||
import { ApiKey } from "@prisma/client";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
|
||||
const apiKeySchema = z.object({
|
||||
name: z.string(),
|
||||
|
@@ -2,12 +2,13 @@
|
||||
|
||||
import AddApiKey from "./add-api-key";
|
||||
import ApiList from "./api-list";
|
||||
import { H1 } from "@usesend/ui";
|
||||
|
||||
export default function ApiKeysPage() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="font-medium">API Keys</h2>
|
||||
<H1>API Keys</H1>
|
||||
<AddApiKey />
|
||||
</div>
|
||||
<ApiList />
|
||||
|
@@ -2,12 +2,13 @@
|
||||
|
||||
import AddApiKey from "./api-keys/add-api-key";
|
||||
import ApiList from "./api-keys/api-list";
|
||||
import { H1 } from "@usesend/ui";
|
||||
|
||||
export default function ApiKeysPage() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="font-medium">API Keys</h2>
|
||||
<H1>API Keys</H1>
|
||||
<AddApiKey />
|
||||
</div>
|
||||
<ApiList />
|
||||
|
@@ -5,8 +5,8 @@ import {
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@unsend/ui/src/card";
|
||||
import { TextWithCopyButton } from "@unsend/ui/src/text-with-copy";
|
||||
} from "@usesend/ui/src/card";
|
||||
import { TextWithCopyButton } from "@usesend/ui/src/text-with-copy";
|
||||
import { env } from "~/env";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
|
||||
import {
|
||||
Form,
|
||||
@@ -19,13 +19,13 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
|
||||
import { api } from "~/trpc/react";
|
||||
import React, { useState } from "react";
|
||||
import { Domain } from "@prisma/client";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -66,7 +66,7 @@ export const DeleteDomain: React.FC<{ domain: Domain }> = ({ domain }) => {
|
||||
toast.success(`Domain ${domain.name} deleted`);
|
||||
router.replace("/domains");
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -9,7 +9,7 @@ import {
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@unsend/ui/src/breadcrumb";
|
||||
} from "@usesend/ui/src/breadcrumb";
|
||||
import { DomainStatusBadge } from "../domain-badge";
|
||||
import {
|
||||
Table,
|
||||
@@ -18,15 +18,16 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@unsend/ui/src/table";
|
||||
import { TextWithCopyButton } from "@unsend/ui/src/text-with-copy";
|
||||
} from "@usesend/ui/src/table";
|
||||
import { TextWithCopyButton } from "@usesend/ui/src/text-with-copy";
|
||||
import React, { use } from "react";
|
||||
import { Switch } from "@unsend/ui/src/switch";
|
||||
import { Switch } from "@usesend/ui/src/switch";
|
||||
import DeleteDomain from "./delete-domain";
|
||||
import SendTestMail from "./send-test-mail";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import Link from "next/link";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { H1 } from "@usesend/ui";
|
||||
|
||||
export default function DomainItemPage({
|
||||
params,
|
||||
@@ -42,7 +43,7 @@ export default function DomainItemPage({
|
||||
{
|
||||
refetchInterval: (q) => (q?.state.data?.isVerifying ? 10000 : false),
|
||||
refetchIntervalInBackground: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const verifyQuery = api.domain.startVerification.useMutation();
|
||||
@@ -54,7 +55,7 @@ export default function DomainItemPage({
|
||||
onSettled: () => {
|
||||
domainQuery.refetch();
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -67,7 +68,7 @@ export default function DomainItemPage({
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* <div className="flex items-center gap-4">
|
||||
<h1 className="font-medium text-2xl">{domainQuery.data?.name}</h1>
|
||||
<H1>{domainQuery.data?.name}</H1>
|
||||
</div> */}
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
@@ -151,7 +152,7 @@ export default function DomainItemPage({
|
||||
<TableCell className="">TXT</TableCell>
|
||||
<TableCell>
|
||||
<TextWithCopyButton
|
||||
value={`unsend._domainkey.${domainQuery.data?.subdomain || domainQuery.data?.name}`}
|
||||
value={`${domainQuery.data?.dkimSelector ?? "unsend"}._domainkey.${domainQuery.data?.subdomain || domainQuery.data?.name}`}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="">
|
||||
@@ -232,7 +233,7 @@ const DomainSettings: React.FC<{ domain: Domain }> = ({ domain }) => {
|
||||
const utils = api.useUtils();
|
||||
|
||||
const [clickTracking, setClickTracking] = React.useState(
|
||||
domain.clickTracking
|
||||
domain.clickTracking,
|
||||
);
|
||||
const [openTracking, setOpenTracking] = React.useState(domain.openTracking);
|
||||
|
||||
@@ -245,7 +246,7 @@ const DomainSettings: React.FC<{ domain: Domain }> = ({ domain }) => {
|
||||
utils.domain.invalidate();
|
||||
toast.success("Click tracking updated");
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -258,7 +259,7 @@ const DomainSettings: React.FC<{ domain: Domain }> = ({ domain }) => {
|
||||
utils.domain.invalidate();
|
||||
toast.success("Open tracking updated");
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
@@ -1,121 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { api } from "~/trpc/react";
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import { Domain } from "@prisma/client";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { SendHorizonal } from "lucide-react";
|
||||
import { Code } from "@unsend/ui/src/code";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { getSendTestEmailCode } from "~/lib/constants/example-codes";
|
||||
|
||||
const jsCode = `const requestOptions = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer us_ad9a79256e366399c747cbf0b38eca3c472e8a2e"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"to": "koushikmohan1996@gmail.com",
|
||||
"from": "hello@test.splitpro.app",
|
||||
"subject": "Test mail",
|
||||
"html": "<p>Hello this is a test mail</p>"
|
||||
}),
|
||||
redirect: "follow"
|
||||
};
|
||||
|
||||
fetch("http://localhost:3000/api/v1/emails", requestOptions)
|
||||
.then(response => response.text())
|
||||
.then(result => console.log(result))
|
||||
.catch(error => console.error(error));
|
||||
`;
|
||||
|
||||
const pythonCode = `import requests
|
||||
import json
|
||||
|
||||
url = "http://localhost:3000/api/v1/emails"
|
||||
|
||||
payload = json.dumps({
|
||||
"to": "koushikmohan1996@gmail.com",
|
||||
"from": "hello@test.splitpro.app",
|
||||
"subject": "Test mail",
|
||||
"html": "<p>Hello this is a test mail</p>"
|
||||
})
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer us_ad9a79256e366399c747cbf0b38eca3c472e8a2e'
|
||||
}
|
||||
|
||||
response = requests.request("POST", url, headers=headers, data=payload)
|
||||
|
||||
print(response.text)`;
|
||||
|
||||
const rubyCode = `require 'uri'
|
||||
require 'net/http'
|
||||
require 'json'
|
||||
|
||||
url = URI("http://localhost:3000/api/v1/emails")
|
||||
|
||||
http = Net::HTTP.new(url.host, url.port)
|
||||
request = Net::HTTP::Post.new(url)
|
||||
request["Accept"] = 'application/json'
|
||||
request["Content-Type"] = 'application/json'
|
||||
request["Authorization"] = 'Bearer us_ad9a79256e366399c747cbf0b38eca3c472e8a2e'
|
||||
request.body = JSON.dump({
|
||||
"to" => "koushikmohan1996@gmail.com",
|
||||
"from" => "hello@test.splitpro.app",
|
||||
"subject" => "Test mail",
|
||||
"html" => "<p>Hello this is a test mail</p>"
|
||||
})
|
||||
|
||||
response = http.request(request)
|
||||
puts response.read_body`;
|
||||
|
||||
const phpCode = `$url = "http://localhost:3000/api/v1/emails";
|
||||
|
||||
$payload = json_encode(array(
|
||||
"to" => "koushikmohan1996@gmail.com",
|
||||
"from" => "hello@test.splitpro.app",
|
||||
"subject" => "Test mail",
|
||||
"html" => "<p>Hello this is a test mail</p>"
|
||||
));
|
||||
|
||||
$headers = array(
|
||||
"Accept: application/json",
|
||||
"Content-Type: application/json",
|
||||
"Authorization: Bearer us_ad9a79256e366399c747cbf0b38eca3c472e8a2e"
|
||||
);
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
if (curl_errno($ch)) {
|
||||
echo 'Error:' . curl_error($ch);
|
||||
} else {
|
||||
echo $response;
|
||||
}`;
|
||||
// Removed dialog and example code. Clicking the button now sends the email directly.
|
||||
|
||||
export const SendTestMail: React.FC<{ domain: Domain }> = ({ domain }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const sendTestEmailFromDomainMutation =
|
||||
api.domain.sendTestEmailFromDomain.useMutation();
|
||||
|
||||
const { data: session } = useSession();
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
function handleSendTestEmail() {
|
||||
@@ -127,50 +23,24 @@ export const SendTestMail: React.FC<{ domain: Domain }> = ({ domain }) => {
|
||||
onSuccess: () => {
|
||||
utils.domain.domains.invalidate();
|
||||
toast.success(`Test email sent`);
|
||||
setOpen(false);
|
||||
},
|
||||
}
|
||||
onError: (err) => {
|
||||
toast.error(err.message || "Failed to send test email");
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(_open) => (_open !== open ? setOpen(_open) : null)}
|
||||
<Button
|
||||
onClick={handleSendTestEmail}
|
||||
disabled={sendTestEmailFromDomainMutation.isPending}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<SendHorizonal className="h-4 w-4 mr-2" />
|
||||
Send test email
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className=" max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Send test email</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Code
|
||||
codeBlocks={getSendTestEmailCode({
|
||||
from: `hello@${domain.name}`,
|
||||
to: session?.user?.email || "",
|
||||
subject: "Unsend test email",
|
||||
body: "hello,\\n\\nUnsend is the best open source sending platform",
|
||||
bodyHtml:
|
||||
"<p>hello,</p><p>Unsend is the best open source sending platform<p><p>check out <a href='https://unsend.dev'>unsend.dev</a>",
|
||||
})}
|
||||
codeClassName="max-w-[38rem] h-[20rem]"
|
||||
/>
|
||||
<div className="flex justify-end w-full">
|
||||
<Button
|
||||
onClick={handleSendTestEmail}
|
||||
disabled={sendTestEmailFromDomainMutation.isPending}
|
||||
>
|
||||
{sendTestEmailFromDomainMutation.isPending
|
||||
? "Sending email..."
|
||||
: "Send test email"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<SendHorizonal className="h-4 w-4 mr-2" />
|
||||
{sendTestEmailFromDomainMutation.isPending
|
||||
? "Sending email..."
|
||||
: "Send test email"}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -17,10 +17,10 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
|
||||
import { api } from "~/trpc/react";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as tldts from "tldts";
|
||||
@@ -33,15 +33,13 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@unsend/ui/src/select";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
} from "@usesend/ui/src/select";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { useUpgradeModalStore } from "~/store/upgradeModalStore";
|
||||
import { LimitReason } from "~/lib/constants/plans";
|
||||
|
||||
const domainSchema = z.object({
|
||||
region: z.string({ required_error: "Region is required" }).min(1, {
|
||||
message: "Region is required",
|
||||
}),
|
||||
region: z.string().optional(),
|
||||
domain: z.string({ required_error: "Domain is required" }).min(1, {
|
||||
message: "Domain is required",
|
||||
}),
|
||||
@@ -68,6 +66,11 @@ export default function AddDomain() {
|
||||
const utils = api.useUtils();
|
||||
const router = useRouter();
|
||||
|
||||
const singleRegion =
|
||||
regionQuery.data?.length === 1 ? regionQuery.data[0] : undefined;
|
||||
|
||||
const showRegionSelect = (regionQuery.data?.length ?? 0) > 1;
|
||||
|
||||
async function onDomainAdd(values: z.infer<typeof domainSchema>) {
|
||||
const domain = tldts.getDomain(values.domain);
|
||||
if (!domain) {
|
||||
@@ -78,6 +81,13 @@ export default function AddDomain() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!values.region && !singleRegion) {
|
||||
domainForm.setError("region", {
|
||||
message: "Region is required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (limitsQuery.data?.isLimitReached) {
|
||||
openModal(limitsQuery.data.reason);
|
||||
return;
|
||||
@@ -86,7 +96,7 @@ export default function AddDomain() {
|
||||
addDomainMutation.mutate(
|
||||
{
|
||||
name: values.domain,
|
||||
region: values.region,
|
||||
region: singleRegion ?? values.region ?? "",
|
||||
},
|
||||
{
|
||||
onSuccess: async (data) => {
|
||||
@@ -151,40 +161,43 @@ export default function AddDomain() {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={domainForm.control}
|
||||
name="region"
|
||||
render={({ field, formState }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Region</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
disabled={regionQuery.isLoading}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select region" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{regionQuery.data?.map((region) => (
|
||||
<SelectItem value={region} key={region}>
|
||||
{region}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{formState.errors.region ? (
|
||||
<FormMessage />
|
||||
) : (
|
||||
<FormDescription>
|
||||
Select the region from where the email is sent{" "}
|
||||
</FormDescription>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{showRegionSelect && (
|
||||
<FormField
|
||||
control={domainForm.control}
|
||||
name="region"
|
||||
render={({ field, formState }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Region</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
disabled={regionQuery.isLoading}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select region" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{regionQuery.data?.map((region) => (
|
||||
<SelectItem value={region} key={region}>
|
||||
{region}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{formState.errors.region ? (
|
||||
<FormMessage />
|
||||
) : (
|
||||
<FormDescription>
|
||||
Select the region from where the email is sent{" "}
|
||||
</FormDescription>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
|
@@ -3,12 +3,12 @@
|
||||
import { Domain } from "@prisma/client";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import Link from "next/link";
|
||||
import { Switch } from "@unsend/ui/src/switch";
|
||||
import { Switch } from "@usesend/ui/src/switch";
|
||||
import { api } from "~/trpc/react";
|
||||
import React from "react";
|
||||
import { StatusIndicator } from "./status-indicator";
|
||||
import { DomainStatusBadge } from "./domain-badge";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
|
||||
export default function DomainsList() {
|
||||
const domainsQuery = api.domain.domains.useQuery();
|
||||
|
@@ -2,12 +2,13 @@
|
||||
|
||||
import DomainsList from "./domain-list";
|
||||
import AddDomain from "./add-domain";
|
||||
import { H1 } from "@usesend/ui";
|
||||
|
||||
export default function DomainsPage() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="font-bold text-lg">Domains</h1>
|
||||
<H1>Domains</H1>
|
||||
<AddDomain />
|
||||
</div>
|
||||
<DomainsList />
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import { api } from "~/trpc/react";
|
||||
import React, { useState } from "react";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
|
||||
const cancelSchema = z.object({
|
||||
confirmation: z.string(),
|
||||
|
@@ -1,27 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import * as chrono from "chrono-node";
|
||||
import { api } from "~/trpc/react";
|
||||
import { useRef, useState } from "react";
|
||||
import { Edit3 } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@unsend/ui/src/dropdown-menu";
|
||||
} from "@usesend/ui/src/dropdown-menu";
|
||||
import {
|
||||
Command,
|
||||
CommandDialog,
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from "@unsend/ui/src/command";
|
||||
} from "@usesend/ui/src/command";
|
||||
|
||||
export const EditSchedule: React.FC<{
|
||||
emailId: string;
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { api } from "~/trpc/react";
|
||||
import { Separator } from "@unsend/ui/src/separator";
|
||||
import { Separator } from "@usesend/ui/src/separator";
|
||||
import { EmailStatusBadge, EmailStatusIcon } from "./email-status-badge";
|
||||
import { formatDate } from "date-fns";
|
||||
import { motion } from "framer-motion";
|
||||
|
@@ -7,7 +7,7 @@ import {
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableCell,
|
||||
} from "@unsend/ui/src/table";
|
||||
} from "@usesend/ui/src/table";
|
||||
import { api } from "~/trpc/react";
|
||||
import {
|
||||
Mail,
|
||||
@@ -23,34 +23,34 @@ import { EmailStatusBadge } from "./email-status-badge";
|
||||
import EmailDetails from "./email-details";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useUrlState } from "~/hooks/useUrlState";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "@unsend/ui/src/select";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
} from "@usesend/ui/src/select";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@unsend/ui/src/tooltip";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
} from "@usesend/ui/src/tooltip";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import { DEFAULT_QUERY_LIMIT } from "~/lib/constants";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { useState } from "react";
|
||||
import { SheetTitle, SheetDescription } from "@unsend/ui/src/sheet";
|
||||
import { SheetTitle, SheetDescription } from "@usesend/ui/src/sheet";
|
||||
|
||||
/* Stupid hydrating error. And I so stupid to understand the stupid NextJS docs */
|
||||
const DynamicSheetWithNoSSR = dynamic(
|
||||
() => import("@unsend/ui/src/sheet").then((mod) => mod.Sheet),
|
||||
() => import("@usesend/ui/src/sheet").then((mod) => mod.Sheet),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
const DynamicSheetContentWithNoSSR = dynamic(
|
||||
() => import("@unsend/ui/src/sheet").then((mod) => mod.SheetContent),
|
||||
() => import("@usesend/ui/src/sheet").then((mod) => mod.SheetContent),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
|
@@ -1,12 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import EmailList from "./email-list";
|
||||
import { H1 } from "@usesend/ui";
|
||||
|
||||
export default function EmailsPage() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="font-semibold text-xl">Emails</h1>
|
||||
<H1>Emails</H1>
|
||||
</div>
|
||||
<EmailList />
|
||||
</div>
|
||||
|
@@ -1,11 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
import { CheckCircle2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { api } from "~/trpc/react";
|
||||
import { H1 } from "@usesend/ui";
|
||||
|
||||
export default function PaymentsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
@@ -15,9 +16,7 @@ export default function PaymentsPage() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-10">
|
||||
<h1 className="text-2xl font-semibold mb-8">
|
||||
Payment {success ? "Success" : canceled ? "Canceled" : "Unknown"}
|
||||
</h1>
|
||||
<H1>Payment {success ? "Success" : canceled ? "Canceled" : "Unknown"}</H1>
|
||||
{canceled ? (
|
||||
<Link href="/settings/billing">
|
||||
<Button>Go to billing</Button>
|
||||
|
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Card } from "@unsend/ui/src/card";
|
||||
import { Spinner } from "@unsend/ui/src/spinner";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Card } from "@usesend/ui/src/card";
|
||||
import { Spinner } from "@usesend/ui/src/spinner";
|
||||
import { format } from "date-fns";
|
||||
import { useTeam } from "~/providers/team-context";
|
||||
import { api } from "~/trpc/react";
|
||||
|
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import { api } from "~/trpc/react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { Trash2 } from "lucide-react";
|
||||
|
||||
export const DeleteTeamInvite: React.FC<{
|
||||
|
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import { api } from "~/trpc/react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { Role } from "@prisma/client";
|
||||
import { LogOut, Trash2 } from "lucide-react";
|
||||
|
||||
|
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
|
||||
import { api } from "~/trpc/react";
|
||||
import { useState } from "react";
|
||||
@@ -23,7 +23,7 @@ import { PencilIcon } from "lucide-react";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { Role } from "@prisma/client";
|
||||
import {
|
||||
Select,
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@unsend/ui/src/select";
|
||||
} from "@usesend/ui/src/select";
|
||||
|
||||
const teamUserSchema = z.object({
|
||||
role: z.enum(["MEMBER", "ADMIN"]),
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -9,18 +9,18 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@unsend/ui/src/select";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
} from "@usesend/ui/src/select";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { api } from "~/trpc/react";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
import { useTeam } from "~/providers/team-context";
|
||||
import { isCloud, isSelfHosted } from "~/utils/common";
|
||||
import { useUpgradeModalStore } from "~/store/upgradeModalStore";
|
||||
|
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { api } from "~/trpc/react";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { Copy, RotateCw } from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@unsend/ui/src/tooltip";
|
||||
} from "@usesend/ui/src/tooltip";
|
||||
import { isSelfHosted } from "~/utils/common";
|
||||
|
||||
export const ResendTeamInvite: React.FC<{
|
||||
|
@@ -7,10 +7,10 @@ import {
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableCell,
|
||||
} from "@unsend/ui/src/table";
|
||||
} from "@usesend/ui/src/table";
|
||||
import { api } from "~/trpc/react";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { Role } from "@prisma/client";
|
||||
import { EditTeamMember } from "./edit-team-member";
|
||||
|
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { api } from "~/trpc/react";
|
||||
import { Card } from "@unsend/ui/src/card";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import { Card } from "@usesend/ui/src/card";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
getCost,
|
||||
@@ -14,7 +14,7 @@ import { useTeam } from "~/providers/team-context";
|
||||
import { EmailUsageType } from "@prisma/client";
|
||||
import { PlanDetails } from "~/components/payments/PlanDetails";
|
||||
import { UpgradeButton } from "~/components/payments/UpgradeButton";
|
||||
import { Progress } from "@unsend/ui/src/progress";
|
||||
import { Progress } from "@usesend/ui/src/progress";
|
||||
|
||||
const FREE_PLAN_LIMIT = 3000;
|
||||
|
||||
@@ -49,7 +49,7 @@ function FreePlanUsage({
|
||||
<div className="text-sm text-muted-foreground mt-1">
|
||||
{item.type === "TRANSACTIONAL"
|
||||
? "Mails sent using the send api or SMTP"
|
||||
: "Mails designed sent from unsend editor"}
|
||||
: "Mails designed sent from useSend editor"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-mono font-medium">
|
||||
|
@@ -10,17 +10,17 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Label } from "@unsend/ui/src/label";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import { Label } from "@usesend/ui/src/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@unsend/ui/src/select";
|
||||
} from "@usesend/ui/src/select";
|
||||
|
||||
interface AddSuppressionDialogProps {
|
||||
open: boolean;
|
||||
|
@@ -10,18 +10,18 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Label } from "@unsend/ui/src/label";
|
||||
import { Textarea } from "@unsend/ui/src/textarea";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Label } from "@usesend/ui/src/label";
|
||||
import { Textarea } from "@usesend/ui/src/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@unsend/ui/src/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@unsend/ui/src/tabs";
|
||||
} from "@usesend/ui/src/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@usesend/ui/src/tabs";
|
||||
import { Upload, FileText } from "lucide-react";
|
||||
|
||||
interface BulkAddSuppressionsDialogProps {
|
||||
|
@@ -5,8 +5,9 @@ import AddSuppressionDialog from "./add-suppression";
|
||||
import BulkAddSuppressionsDialog from "./bulk-add-suppressions";
|
||||
import SuppressionList from "./suppression-list";
|
||||
import SuppressionStats from "./suppression-stats";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Plus, Upload } from "lucide-react";
|
||||
import { H1 } from "@usesend/ui";
|
||||
|
||||
export default function SuppressionsPage() {
|
||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||
@@ -16,7 +17,7 @@ export default function SuppressionsPage() {
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="font-bold text-lg">Suppression List</h1>
|
||||
<H1>Suppression List</H1>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => setShowBulkAddDialog(true)}>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
|
@@ -7,8 +7,8 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
|
||||
interface RemoveSuppressionDialogProps {
|
||||
email: string | null;
|
||||
|
@@ -6,15 +6,15 @@ import { useUrlState } from "~/hooks/useUrlState";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { SuppressionReason } from "@prisma/client";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@unsend/ui/src/select";
|
||||
} from "@usesend/ui/src/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -22,10 +22,10 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@unsend/ui/src/table";
|
||||
} from "@usesend/ui/src/table";
|
||||
import { Trash2, Download } from "lucide-react";
|
||||
import RemoveSuppressionDialog from "./remove-suppression";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
|
||||
const reasonLabels = {
|
||||
HARD_BOUNCE: "Hard Bounce",
|
||||
|
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { api } from "~/trpc/react";
|
||||
import { Spinner } from "@unsend/ui/src/spinner";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Editor } from "@unsend/email-editor";
|
||||
import { Spinner } from "@usesend/ui/src/spinner";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import { Editor } from "@usesend/email-editor";
|
||||
import { useState } from "react";
|
||||
import { Template } from "@prisma/client";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
|
||||
import { api } from "~/trpc/react";
|
||||
import { useState } from "react";
|
||||
@@ -24,9 +24,9 @@ import { Plus } from "lucide-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
|
||||
const templateSchema = z.object({
|
||||
name: z.string({ required_error: "Name is required" }).min(1, {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import { api } from "~/trpc/react";
|
||||
import React, { useState } from "react";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
import { Template } from "@prisma/client";
|
||||
|
||||
const templateSchema = z.object({
|
||||
|
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import { api } from "~/trpc/react";
|
||||
import React, { useState } from "react";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { Copy } from "lucide-react";
|
||||
import { Template } from "@prisma/client";
|
||||
|
||||
|
@@ -2,12 +2,13 @@
|
||||
|
||||
import TemplateList from "./template-list";
|
||||
import CreateTemplate from "./create-template";
|
||||
import { H1 } from "@usesend/ui";
|
||||
|
||||
export default function TemplatesPage() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="font-bold text-lg">Templates</h1>
|
||||
<H1>Templates</H1>
|
||||
<CreateTemplate />
|
||||
</div>
|
||||
<TemplateList />
|
||||
|
@@ -7,17 +7,17 @@ import {
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableCell,
|
||||
} from "@unsend/ui/src/table";
|
||||
} from "@usesend/ui/src/table";
|
||||
import { api } from "~/trpc/react";
|
||||
import { useUrlState } from "~/hooks/useUrlState";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
// import DeleteCampaign from "./delete-campaign";
|
||||
import Link from "next/link";
|
||||
// import DuplicateCampaign from "./duplicate-campaign";
|
||||
|
||||
import { TextWithCopyButton } from "@unsend/ui/src/text-with-copy";
|
||||
import { TextWithCopyButton } from "@usesend/ui/src/text-with-copy";
|
||||
import DeleteTemplate from "./delete-template";
|
||||
import DuplicateTemplate from "./duplicate-template";
|
||||
|
||||
|
@@ -18,13 +18,13 @@ export async function GET(request: NextRequest) {
|
||||
if (type === "otp") {
|
||||
html = await renderOtpEmail({
|
||||
otpCode: "ABC123",
|
||||
loginUrl: "https://app.unsend.dev/login?token=abc123",
|
||||
hostName: "Unsend",
|
||||
loginUrl: "https://app.usesend.com/login?token=abc123",
|
||||
hostName: "useSend",
|
||||
});
|
||||
} else if (type === "invite") {
|
||||
html = await renderTeamInviteEmail({
|
||||
teamName: "My Awesome Team",
|
||||
inviteUrl: "https://app.unsend.dev/join-team?inviteId=123",
|
||||
inviteUrl: "https://app.usesend.com/join-team?inviteId=123",
|
||||
inviterName: "John Doe",
|
||||
role: "admin",
|
||||
});
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { EmailRenderer } from "@unsend/email-editor/src/renderer";
|
||||
import { EmailRenderer } from "@usesend/email-editor/src/renderer";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -11,7 +11,8 @@ export async function POST(req: Request) {
|
||||
const html = await renderer.render({
|
||||
shouldReplaceVariableValues: true,
|
||||
linkValues: {
|
||||
"{{unsend_unsubscribe_url}}": "https://unsend.com/unsubscribe",
|
||||
"{{usesend_unsubscribe_url}}": "https://usesend.com/unsubscribe",
|
||||
"{{unsend_unsubscribe_url}}": "https://usesend.com/unsubscribe",
|
||||
},
|
||||
});
|
||||
console.log(`Time taken: ${Date.now() - time}ms`);
|
||||
@@ -34,7 +35,7 @@ export async function POST(req: Request) {
|
||||
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type",
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import JoinTeam from "~/components/team/JoinTeam";
|
||||
import { Suspense } from "react";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
import { getServerAuthSession } from "~/server/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import "@unsend/ui/styles/globals.css";
|
||||
import "@usesend/ui/styles/globals.css";
|
||||
|
||||
import { Inter } from "next/font/google";
|
||||
import { ThemeProvider } from "@unsend/ui";
|
||||
import { Toaster } from "@unsend/ui/src/toaster";
|
||||
import { JetBrains_Mono } from "next/font/google";
|
||||
import { ThemeProvider } from "@usesend/ui";
|
||||
import { Toaster } from "@usesend/ui/src/toaster";
|
||||
|
||||
import { TRPCReactProvider } from "~/trpc/react";
|
||||
import { Metadata } from "next";
|
||||
@@ -12,9 +13,14 @@ const inter = Inter({
|
||||
variable: "--font-sans",
|
||||
});
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-mono",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Unsend",
|
||||
description: "Open source sending infrastructure for developers",
|
||||
title: "useSend",
|
||||
description: "Open source email platoform",
|
||||
icons: [{ rel: "icon", url: "/favicon.ico" }],
|
||||
};
|
||||
|
||||
@@ -25,7 +31,9 @@ export default async function RootLayout({
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning className="bg-sidebar-background">
|
||||
<body className={`font-sans ${inter.variable} app bg-sidebar-background`}>
|
||||
<body
|
||||
className={`font-sans ${inter.variable} ${jetbrainsMono.variable} app bg-sidebar-background`}
|
||||
>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<Toaster />
|
||||
<TRPCReactProvider>{children}</TRPCReactProvider>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import Image from "next/image";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -14,18 +14,18 @@ import {
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
REGEXP_ONLY_DIGITS_AND_CHARS,
|
||||
} from "@unsend/ui/src/input-otp";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
} from "@usesend/ui/src/input-otp";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import { BuiltInProviderType } from "next-auth/providers/index";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
import Link from "next/link";
|
||||
import { useTheme } from "@unsend/ui";
|
||||
import { useTheme } from "@usesend/ui";
|
||||
import { useSearchParams as useNextSearchParams } from "next/navigation";
|
||||
|
||||
const emailSchema = z.object({
|
||||
@@ -94,7 +94,9 @@ export default function LoginPage({
|
||||
const email = emailForm.getValues().email;
|
||||
console.log("email", email);
|
||||
|
||||
const finalCallbackUrl = inviteId ? `/join-team?inviteId=${inviteId}` : `${callbackUrl}/dashboard`;
|
||||
const finalCallbackUrl = inviteId
|
||||
? `/join-team?inviteId=${inviteId}`
|
||||
: `${callbackUrl}/dashboard`;
|
||||
window.location.href = `/api/auth/callback/email?email=${encodeURIComponent(
|
||||
email.toLowerCase()
|
||||
)}&token=${values.otp.toLowerCase()}&callbackUrl=${encodeURIComponent(finalCallbackUrl)}`;
|
||||
@@ -106,13 +108,15 @@ export default function LoginPage({
|
||||
|
||||
const [submittedProvider, setSubmittedProvider] =
|
||||
useState<LiteralUnion<BuiltInProviderType> | null>(null);
|
||||
|
||||
|
||||
const searchParams = useNextSearchParams();
|
||||
const inviteId = searchParams.get("inviteId");
|
||||
|
||||
const handleSubmit = (provider: LiteralUnion<BuiltInProviderType>) => {
|
||||
setSubmittedProvider(provider);
|
||||
const callbackUrl = inviteId ? `/join-team?inviteId=${inviteId}` : "/dashboard";
|
||||
const callbackUrl = inviteId
|
||||
? `/join-team?inviteId=${inviteId}`
|
||||
: "/dashboard";
|
||||
signIn(provider, { callbackUrl });
|
||||
};
|
||||
|
||||
@@ -122,18 +126,18 @@ export default function LoginPage({
|
||||
<main className="h-screen flex justify-center items-center">
|
||||
<div className="flex flex-col gap-6">
|
||||
<Image
|
||||
src={resolvedTheme === "dark" ? "/logo-dark.png" : "/logo-light.png"}
|
||||
alt="Unsend"
|
||||
src={"/logo-squircle.png"}
|
||||
alt="useSend"
|
||||
width={50}
|
||||
height={50}
|
||||
className="mx-auto"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-2xl text-center font-semibold">
|
||||
{isSignup ? "Create new account" : "Sign into Unsend"}
|
||||
{isSignup ? "Create new account" : "Sign into useSend"}
|
||||
</p>
|
||||
<p className="text-center mt-2 text-sm text-muted-foreground">
|
||||
{isSignup ? "Already have an account?" : "New to Unsend?"}
|
||||
{isSignup ? "Already have an account?" : "New to useSend?"}
|
||||
<Link
|
||||
href={isSignup ? "/login" : "/signup"}
|
||||
className=" text-foreground hover:underline ml-1"
|
||||
|
@@ -37,8 +37,8 @@ async function UnsubscribePage({
|
||||
<div className=" fixed bottom-10 p-4">
|
||||
<p>
|
||||
Powered by{" "}
|
||||
<a href="https://unsend.dev" className="font-bold" target="_blank">
|
||||
Unsend
|
||||
<a href="https://usesend.com" className="font-bold" target="_blank">
|
||||
useSend
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Contact } from "@prisma/client";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { useState } from "react";
|
||||
import { api } from "~/trpc/react";
|
||||
|
||||
|
@@ -31,14 +31,14 @@ import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@unsend/ui/src/sidebar";
|
||||
} from "@usesend/ui/src/sidebar";
|
||||
import Link from "next/link";
|
||||
import { MiniThemeSwitcher, ThemeSwitcher } from "./theme/ThemeSwitcher";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { isSelfHosted } from "~/utils/common";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Badge } from "@unsend/ui/src/badge";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@unsend/ui/src/avatar";
|
||||
import { Badge } from "@usesend/ui/src/badge";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@usesend/ui/src/avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@unsend/ui/src/dropdown-menu";
|
||||
} from "@usesend/ui/src/dropdown-menu";
|
||||
|
||||
// General items
|
||||
const generalItems = [
|
||||
@@ -125,8 +125,8 @@ export function AppSidebar() {
|
||||
<SidebarHeader>
|
||||
<SidebarGroupLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg font-semibold text-foreground">
|
||||
Unsend
|
||||
<span className="text-lg font-semibold text-foreground font-mono">
|
||||
useSend
|
||||
</span>
|
||||
<Badge variant="outline">Beta</Badge>
|
||||
</div>
|
||||
@@ -234,7 +234,7 @@ export function AppSidebar() {
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild tooltip="Docs">
|
||||
<Link href="https://docs.unsend.dev" target="_blank">
|
||||
<Link href="https://docs.usesend.com" target="_blank">
|
||||
<BookOpenText />
|
||||
<span>Docs</span>
|
||||
</Link>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useTheme } from "@unsend/ui";
|
||||
import { useTheme } from "@usesend/ui";
|
||||
import Image from "next/image";
|
||||
|
||||
export const FullScreenLoading = () => {
|
||||
@@ -6,8 +6,8 @@ export const FullScreenLoading = () => {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<Image
|
||||
src={"/logo-light.png"}
|
||||
alt="Unsend"
|
||||
src={"/logo-squircle.png"}
|
||||
alt="useSend"
|
||||
width={45}
|
||||
height={45}
|
||||
className="mx-auto"
|
||||
|
@@ -2,9 +2,9 @@ import { Plan } from "@prisma/client";
|
||||
import { PLAN_PERKS } from "~/lib/constants/payments";
|
||||
import { CheckCircle2 } from "lucide-react";
|
||||
import { api } from "~/trpc/react";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
import { useTeam } from "~/providers/team-context";
|
||||
import { Badge } from "@unsend/ui/src/badge";
|
||||
import { Badge } from "@usesend/ui/src/badge";
|
||||
import { format } from "date-fns";
|
||||
|
||||
export const PlanDetails = () => {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
import { api } from "~/trpc/react";
|
||||
|
||||
export const UpgradeButton = () => {
|
||||
|
@@ -6,7 +6,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import { CheckCircle2 } from "lucide-react";
|
||||
import { useUpgradeModalStore } from "~/store/upgradeModalStore";
|
||||
import { PLAN_PERKS } from "~/lib/constants/payments";
|
||||
|
@@ -9,19 +9,19 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
} from "@usesend/ui/src/form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { api } from "~/trpc/react";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import Spinner from "@usesend/ui/src/spinner";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { isLocalhost } from "~/utils/client";
|
||||
|
||||
const FormSchema = z.object({
|
||||
region: z.string(),
|
||||
unsendUrl: z.string().url(),
|
||||
usesendUrl: z.string().url(),
|
||||
sendRate: z.coerce.number(),
|
||||
transactionalQuota: z.coerce.number().min(0).max(100),
|
||||
});
|
||||
@@ -56,7 +56,7 @@ export const AddSesSettingsForm: React.FC<SesSettingsProps> = ({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
region: "",
|
||||
unsendUrl: "",
|
||||
usesendUrl: "",
|
||||
sendRate: 1,
|
||||
transactionalQuota: 50,
|
||||
},
|
||||
@@ -65,31 +65,39 @@ export const AddSesSettingsForm: React.FC<SesSettingsProps> = ({
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
const localhost = isLocalhost();
|
||||
|
||||
if (!data.unsendUrl.startsWith("https://") && !localhost) {
|
||||
form.setError("unsendUrl", {
|
||||
if (!data.usesendUrl.startsWith("https://") && !localhost) {
|
||||
form.setError("usesendUrl", {
|
||||
message: "URL must start with https://",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.unsendUrl.includes("localhost") && !localhost) {
|
||||
form.setError("unsendUrl", {
|
||||
if (data.usesendUrl.includes("localhost") && !localhost) {
|
||||
form.setError("usesendUrl", {
|
||||
message: "URL must be a valid url",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
addSesSettings.mutate(data, {
|
||||
onSuccess: () => {
|
||||
utils.admin.invalidate();
|
||||
onSuccess?.();
|
||||
addSesSettings.mutate(
|
||||
{
|
||||
region: data.region,
|
||||
usesendUrl: data.usesendUrl,
|
||||
sendRate: data.sendRate,
|
||||
transactionalQuota: data.transactionalQuota,
|
||||
},
|
||||
onError: (e) => {
|
||||
toast.error("Failed to create", {
|
||||
description: e.message,
|
||||
});
|
||||
{
|
||||
onSuccess: () => {
|
||||
utils.admin.invalidate();
|
||||
onSuccess?.();
|
||||
},
|
||||
onError: (e) => {
|
||||
toast.error("Failed to create", {
|
||||
description: e.message,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
const onRegionInputOutOfFocus = async () => {
|
||||
@@ -134,7 +142,7 @@ export const AddSesSettingsForm: React.FC<SesSettingsProps> = ({
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="unsendUrl"
|
||||
name="usesendUrl"
|
||||
render={({ field, formState }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Callback URL</FormLabel>
|
||||
@@ -145,7 +153,7 @@ export const AddSesSettingsForm: React.FC<SesSettingsProps> = ({
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
{formState.errors.unsendUrl ? (
|
||||
{formState.errors.usesendUrl ? (
|
||||
<FormMessage />
|
||||
) : (
|
||||
<FormDescription>
|
||||
|
@@ -4,7 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -12,12 +12,12 @@ import {
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Spinner } from "@unsend/ui/src/spinner";
|
||||
} from "@usesend/ui/src/form";
|
||||
import { Input } from "@usesend/ui/src/input";
|
||||
import { Spinner } from "@usesend/ui/src/spinner";
|
||||
import { api } from "~/trpc/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import JoinTeam from "./JoinTeam";
|
||||
|
||||
const FormSchema = z.object({
|
||||
|
@@ -1,17 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { Spinner } from "@unsend/ui/src/spinner";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Spinner } from "@usesend/ui/src/spinner";
|
||||
import { api } from "~/trpc/react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
} from "@usesend/ui/src/dialog";
|
||||
import { useState } from "react";
|
||||
import type { AppRouter } from "~/server/api/root";
|
||||
import type { inferRouterOutputs } from "@trpc/server";
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { cn, useTheme } from "@unsend/ui";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { cn, useTheme } from "@usesend/ui";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { Monitor, Sun, Moon, SunMoonIcon } from "lucide-react";
|
||||
|
||||
export const ThemeSwitcher = () => {
|
||||
@@ -17,7 +17,7 @@ export const ThemeSwitcher = () => {
|
||||
size="sm"
|
||||
className={cn(
|
||||
"p-0.5 rounded-[0.20rem] h-5 w-5",
|
||||
theme === "system" ? " bg-muted" : ""
|
||||
theme === "system" ? " bg-muted" : "",
|
||||
)}
|
||||
onClick={() => setTheme("system")}
|
||||
>
|
||||
@@ -28,7 +28,7 @@ export const ThemeSwitcher = () => {
|
||||
size="sm"
|
||||
className={cn(
|
||||
"p-0.5 rounded-[0.20rem] h-5 w-5",
|
||||
theme === "light" ? " bg-muted" : ""
|
||||
theme === "light" ? " bg-muted" : "",
|
||||
)}
|
||||
onClick={() => setTheme("light")}
|
||||
>
|
||||
@@ -39,7 +39,7 @@ export const ThemeSwitcher = () => {
|
||||
size="sm"
|
||||
className={cn(
|
||||
"p-0.5 rounded-[0.20rem] h-5 w-5",
|
||||
theme === "dark" ? "bg-muted" : ""
|
||||
theme === "dark" ? "bg-muted" : "",
|
||||
)}
|
||||
onClick={() => setTheme("dark")}
|
||||
>
|
||||
|
@@ -13,7 +13,7 @@ export const env = createEnv({
|
||||
.url()
|
||||
.refine(
|
||||
(str) => !str.includes("YOUR_MYSQL_URL_HERE"),
|
||||
"You forgot to change the default URL"
|
||||
"You forgot to change the default URL",
|
||||
),
|
||||
NODE_ENV: z
|
||||
.enum(["development", "test", "production"])
|
||||
@@ -27,12 +27,13 @@ export const env = createEnv({
|
||||
// Since NextAuth.js automatically uses the VERCEL_URL if present.
|
||||
(str) => process.env.VERCEL_URL ?? str,
|
||||
// VERCEL_URL doesn't include `https` so it cant be validated as a URL
|
||||
process.env.VERCEL ? z.string() : z.string().url()
|
||||
process.env.VERCEL ? z.string() : z.string().url(),
|
||||
),
|
||||
GITHUB_ID: z.string().optional(),
|
||||
GITHUB_SECRET: z.string().optional(),
|
||||
AWS_ACCESS_KEY: z.string(),
|
||||
AWS_SECRET_KEY: z.string(),
|
||||
USESEND_API_KEY: z.string().optional(),
|
||||
UNSEND_API_KEY: z.string().optional(),
|
||||
GOOGLE_CLIENT_ID: z.string().optional(),
|
||||
GOOGLE_CLIENT_SECRET: z.string().optional(),
|
||||
@@ -56,8 +57,8 @@ export const env = createEnv({
|
||||
STRIPE_BASIC_USAGE_PRICE_ID: z.string().optional(),
|
||||
STRIPE_LEGACY_BASIC_PRICE_ID: z.string().optional(),
|
||||
STRIPE_WEBHOOK_SECRET: z.string().optional(),
|
||||
SMTP_HOST: z.string().default("smtp.unsend.dev"),
|
||||
SMTP_USER: z.string().default("unsend"),
|
||||
SMTP_HOST: z.string().default("smtp.usesend.com"),
|
||||
SMTP_USER: z.string().default("usesend"),
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -86,6 +87,7 @@ export const env = createEnv({
|
||||
GITHUB_SECRET: process.env.GITHUB_SECRET,
|
||||
AWS_ACCESS_KEY: process.env.AWS_ACCESS_KEY,
|
||||
AWS_SECRET_KEY: process.env.AWS_SECRET_KEY,
|
||||
USESEND_API_KEY: process.env.USESEND_API_KEY,
|
||||
UNSEND_API_KEY: process.env.UNSEND_API_KEY,
|
||||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
||||
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { CodeBlock } from "@unsend/ui/src/code";
|
||||
import { CodeBlock } from "@usesend/ui/src/code";
|
||||
|
||||
export const getSendTestEmailCode = ({
|
||||
from,
|
||||
@@ -17,13 +17,13 @@ export const getSendTestEmailCode = ({
|
||||
{
|
||||
language: "js",
|
||||
title: "Node.js",
|
||||
code: `import { Unsend } from "unsend";
|
||||
code: `import { UseSend } from "usesend";
|
||||
|
||||
const unsend = new Unsend("us_12345");
|
||||
const usesend = new UseSend("us_12345");
|
||||
|
||||
// const unsend = new Unsend("us_12345", "https://my-unsend-instance.com");
|
||||
// const usesend = new UseSend("us_12345", "https://app.usesend.com");
|
||||
|
||||
unsend.emails.send({
|
||||
usesend.emails.send({
|
||||
to: "${to}",
|
||||
from: "${from}",
|
||||
subject: "${subject}",
|
||||
@@ -37,7 +37,7 @@ unsend.emails.send({
|
||||
title: "Python",
|
||||
code: `import requests
|
||||
|
||||
url = "https://app.unsend.dev/api/v1/emails"
|
||||
url = "https://app.usesend.com/api/v1/emails"
|
||||
|
||||
payload = {
|
||||
"to": "${to}",
|
||||
@@ -63,7 +63,7 @@ response = requests.request("POST", url, json=payload, headers=headers)
|
||||
$curl = curl_init();
|
||||
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_URL => "https://app.unsend.dev/api/v1/emails",
|
||||
CURLOPT_URL => "https://app.usesend.com/api/v1/emails",
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_ENCODING => "",
|
||||
CURLOPT_MAXREDIRS => 10,
|
||||
@@ -97,7 +97,7 @@ if ($err) {
|
||||
require 'uri'
|
||||
require 'json'
|
||||
|
||||
url = URI("https://app.unsend.dev/api/v1/emails")
|
||||
url = URI("https://app.usesend.com/api/v1/emails")
|
||||
|
||||
payload = {
|
||||
"to" => "${to}",
|
||||
@@ -126,7 +126,7 @@ puts response.body
|
||||
{
|
||||
language: "curl",
|
||||
title: "cURL",
|
||||
code: `curl -X POST https://app.unsend.dev/api/v1/emails \\
|
||||
code: `curl -X POST https://app.usesend.com/api/v1/emails \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "Authorization: Bearer us_12345" \\
|
||||
-d '{"to": "${to}", "from": "${from}", "subject": "${subject}", "text": "${body}", "html": "${bodyHtml}"}'`,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
export const DELIVERY_DELAY_ERRORS = {
|
||||
InternalFailure: "An internal Unsend issue caused the message to be delayed.",
|
||||
InternalFailure: "An internal useSend issue caused the message to be delayed.",
|
||||
General: "A generic failure occurred during the SMTP conversation.",
|
||||
MailboxFull:
|
||||
"The recipient's mailbox is full and is unable to receive additional messages.",
|
||||
@@ -12,36 +12,36 @@ export const DELIVERY_DELAY_ERRORS = {
|
||||
TransientCommunicationFailure:
|
||||
"There was a temporary communication failure during the SMTP conversation with the recipient's email provider.",
|
||||
BYOIPHostNameLookupUnavailable:
|
||||
"Unsend was unable to look up the DNS hostname for your IP addresses. This type of delay only occurs when you use Bring Your Own IP.",
|
||||
"useSend was unable to look up the DNS hostname for your IP addresses. This type of delay only occurs when you use Bring Your Own IP.",
|
||||
Undetermined:
|
||||
"Unsend wasn't able to determine the reason for the delivery delay.",
|
||||
"useSend wasn't able to determine the reason for the delivery delay.",
|
||||
SendingDeferral:
|
||||
"Unsend has deemed it appropriate to internally defer the message.",
|
||||
"useSend has deemed it appropriate to internally defer the message.",
|
||||
};
|
||||
|
||||
export const BOUNCE_ERROR_MESSAGES = {
|
||||
Undetermined: "Unsend was unable to determine a specific bounce reason.",
|
||||
Undetermined: "useSend was unable to determine a specific bounce reason.",
|
||||
Permanent: {
|
||||
General:
|
||||
"Unsend received a general hard bounce. If you receive this type of bounce, you should remove the recipient's email address from your mailing list.",
|
||||
"useSend received a general hard bounce. If you receive this type of bounce, you should remove the recipient's email address from your mailing list.",
|
||||
NoEmail:
|
||||
"Unsend received a permanent hard bounce because the target email address does not exist. If you receive this type of bounce, you should remove the recipient's email address from your mailing list.",
|
||||
"useSend received a permanent hard bounce because the target email address does not exist. If you receive this type of bounce, you should remove the recipient's email address from your mailing list.",
|
||||
Suppressed:
|
||||
"Unsend has suppressed sending to this address because it has a recent history of bouncing as an invalid address. To override the global suppression list, see Using the Unsend account-level suppression list.",
|
||||
"useSend has suppressed sending to this address because it has a recent history of bouncing as an invalid address. To override the global suppression list, see Using the useSend account-level suppression list.",
|
||||
OnAccountSuppressionList:
|
||||
"Unsend has suppressed sending to this address because it is on the account-level suppression list. This does not count toward your bounce rate metric.",
|
||||
"useSend has suppressed sending to this address because it is on the account-level suppression list. This does not count toward your bounce rate metric.",
|
||||
},
|
||||
Transient: {
|
||||
General:
|
||||
"Unsend received a general bounce. You may be able to successfully send to this recipient in the future.",
|
||||
"useSend received a general bounce. You may be able to successfully send to this recipient in the future.",
|
||||
MailboxFull:
|
||||
"Unsend received a mailbox full bounce. You may be able to successfully send to this recipient in the future.",
|
||||
"useSend received a mailbox full bounce. You may be able to successfully send to this recipient in the future.",
|
||||
MessageTooLarge:
|
||||
"Unsend received a message too large bounce. You may be able to successfully send to this recipient if you reduce the size of the message.",
|
||||
"useSend received a message too large bounce. You may be able to successfully send to this recipient if you reduce the size of the message.",
|
||||
ContentRejected:
|
||||
"Unsend received a content rejected bounce. You may be able to successfully send to this recipient if you change the content of the message.",
|
||||
"useSend received a content rejected bounce. You may be able to successfully send to this recipient if you change the content of the message.",
|
||||
AttachmentRejected:
|
||||
"Unsend received an attachment rejected bounce. You may be able to successfully send to this recipient if you remove or change the attachment.",
|
||||
"useSend received an attachment rejected bounce. You may be able to successfully send to this recipient if you remove or change the attachment.",
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -14,7 +14,7 @@ export const adminRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
region: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
const acc = await getAccount(input.region);
|
||||
@@ -25,15 +25,15 @@ export const adminRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
region: z.string(),
|
||||
unsendUrl: z.string().url(),
|
||||
usesendUrl: z.string().url(),
|
||||
sendRate: z.number(),
|
||||
transactionalQuota: z.number(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return SesSettingsService.createSesSetting({
|
||||
region: input.region,
|
||||
unsendUrl: input.unsendUrl,
|
||||
usesendUrl: input.usesendUrl,
|
||||
sendingRateLimit: input.sendRate,
|
||||
transactionalQuota: input.transactionalQuota,
|
||||
});
|
||||
@@ -45,7 +45,7 @@ export const adminRouter = createTRPCRouter({
|
||||
settingsId: z.string(),
|
||||
sendRate: z.number(),
|
||||
transactionalQuota: z.number(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return SesSettingsService.updateSesSetting({
|
||||
@@ -59,11 +59,11 @@ export const adminRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
region: z.string().optional().nullable(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return SesSettingsService.getSetting(
|
||||
input.region ?? env.AWS_DEFAULT_REGION
|
||||
input.region ?? env.AWS_DEFAULT_REGION,
|
||||
);
|
||||
}),
|
||||
});
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { CampaignStatus, Prisma } from "@prisma/client";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { EmailRenderer } from "@unsend/email-editor/src/renderer";
|
||||
import { EmailRenderer } from "@usesend/email-editor/src/renderer";
|
||||
import { z } from "zod";
|
||||
import { env } from "~/env";
|
||||
import {
|
||||
@@ -29,7 +29,7 @@ export const campaignRouter = createTRPCRouter({
|
||||
z.object({
|
||||
page: z.number().optional(),
|
||||
status: z.enum(statuses).optional().nullable(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx: { db, team }, input }) => {
|
||||
let completeTime = performance.now();
|
||||
@@ -68,14 +68,14 @@ export const campaignRouter = createTRPCRouter({
|
||||
|
||||
campaignsP.then((campaigns) => {
|
||||
logger.info(
|
||||
`Time taken to get campaigns: ${performance.now() - time} milliseconds`
|
||||
`Time taken to get campaigns: ${performance.now() - time} milliseconds`,
|
||||
);
|
||||
});
|
||||
|
||||
const [campaigns, count] = await Promise.all([campaignsP, countP]);
|
||||
logger.info(
|
||||
{ duration: performance.now() - completeTime },
|
||||
`Time taken to complete request`
|
||||
`Time taken to complete request`,
|
||||
);
|
||||
|
||||
return { campaigns, totalPage: Math.ceil(count / limit) };
|
||||
@@ -87,7 +87,7 @@ export const campaignRouter = createTRPCRouter({
|
||||
name: z.string(),
|
||||
from: z.string(),
|
||||
subject: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx: { db, team }, input }) => {
|
||||
const domain = await validateDomainFromEmail(input.from, team.id);
|
||||
@@ -113,7 +113,7 @@ export const campaignRouter = createTRPCRouter({
|
||||
content: z.string().optional(),
|
||||
contactBookId: z.string().optional(),
|
||||
replyTo: z.string().array().optional(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx: { db, team, campaign: campaignOld }, input }) => {
|
||||
const { campaignId, ...data } = input;
|
||||
@@ -161,7 +161,7 @@ export const campaignRouter = createTRPCRouter({
|
||||
where: { id: input.campaignId, teamId: team.id },
|
||||
});
|
||||
return campaign;
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
getCampaign: campaignProcedure.query(async ({ ctx: { db, team }, input }) => {
|
||||
@@ -194,7 +194,7 @@ export const campaignRouter = createTRPCRouter({
|
||||
sendCampaign: campaignProcedure.mutation(
|
||||
async ({ ctx: { db, team }, input }) => {
|
||||
await sendCampaign(input.campaignId);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
reSubscribeContact: publicProcedure
|
||||
@@ -202,7 +202,7 @@ export const campaignRouter = createTRPCRouter({
|
||||
z.object({
|
||||
id: z.string(),
|
||||
hash: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx: { db }, input }) => {
|
||||
await subscribeContact(input.id, input.hash);
|
||||
@@ -223,7 +223,7 @@ export const campaignRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
return newCampaign;
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
generateImagePresignedUrl: campaignProcedure
|
||||
@@ -231,7 +231,7 @@ export const campaignRouter = createTRPCRouter({
|
||||
z.object({
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx: { team }, input }) => {
|
||||
const extension = input.name.split(".").pop();
|
||||
@@ -239,7 +239,7 @@ export const campaignRouter = createTRPCRouter({
|
||||
|
||||
const url = await getDocumentUploadUrl(
|
||||
`${team.id}/${randomName}`,
|
||||
input.type
|
||||
input.type,
|
||||
);
|
||||
|
||||
const imageUrl = `${env.S3_COMPATIBLE_PUBLIC_URL}/${team.id}/${randomName}`;
|
||||
|
@@ -1,17 +1,17 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { EmailRenderer } from "@unsend/email-editor/src/renderer";
|
||||
import { EmailRenderer } from "@usesend/email-editor/src/renderer";
|
||||
import { z } from "zod";
|
||||
import { env } from "~/env";
|
||||
import {
|
||||
teamProcedure,
|
||||
createTRPCRouter,
|
||||
templateProcedure
|
||||
templateProcedure,
|
||||
} from "~/server/api/trpc";
|
||||
import { nanoid } from "~/server/nanoid";
|
||||
import {
|
||||
getDocumentUploadUrl,
|
||||
isStorageConfigured
|
||||
isStorageConfigured,
|
||||
} from "~/server/service/storage-service";
|
||||
|
||||
export const templateRouter = createTRPCRouter({
|
||||
@@ -19,19 +19,17 @@ export const templateRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
page: z.number().optional(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx: { db, team }, input }) => {
|
||||
const page = input.page || 1;
|
||||
const limit = 30;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
|
||||
const whereConditions: Prisma.TemplateFindManyArgs["where"] = {
|
||||
teamId: team.id,
|
||||
};
|
||||
|
||||
|
||||
const countP = db.template.count({ where: whereConditions });
|
||||
|
||||
const templatesP = db.template.findMany({
|
||||
@@ -61,7 +59,7 @@ export const templateRouter = createTRPCRouter({
|
||||
z.object({
|
||||
name: z.string(),
|
||||
subject: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx: { db, team }, input }) => {
|
||||
const template = await db.template.create({
|
||||
@@ -80,7 +78,7 @@ export const templateRouter = createTRPCRouter({
|
||||
name: z.string().optional(),
|
||||
subject: z.string().optional(),
|
||||
content: z.string().optional(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx: { db }, input }) => {
|
||||
const { templateId, ...data } = input;
|
||||
@@ -109,7 +107,7 @@ export const templateRouter = createTRPCRouter({
|
||||
where: { id: input.templateId, teamId: team.id },
|
||||
});
|
||||
return template;
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
getTemplate: templateProcedure.query(async ({ ctx: { db, team }, input }) => {
|
||||
@@ -139,12 +137,12 @@ export const templateRouter = createTRPCRouter({
|
||||
name: `${template.name} (Copy)`,
|
||||
subject: template.subject,
|
||||
content: template.content,
|
||||
teamId: team.id
|
||||
teamId: team.id,
|
||||
},
|
||||
});
|
||||
|
||||
return newTemplate;
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
generateImagePresignedUrl: templateProcedure
|
||||
@@ -152,7 +150,7 @@ export const templateRouter = createTRPCRouter({
|
||||
z.object({
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx: { team }, input }) => {
|
||||
const extension = input.name.split(".").pop();
|
||||
@@ -160,7 +158,7 @@ export const templateRouter = createTRPCRouter({
|
||||
|
||||
const url = await getDocumentUploadUrl(
|
||||
`${team.id}/${randomName}`,
|
||||
input.type
|
||||
input.type,
|
||||
);
|
||||
|
||||
const imageUrl = `${env.S3_COMPATIBLE_PUBLIC_URL}/${team.id}/${randomName}`;
|
||||
|
@@ -85,7 +85,8 @@ function generateKeyPair() {
|
||||
export async function addDomain(
|
||||
domain: string,
|
||||
region: string,
|
||||
sesTenantId?: string
|
||||
sesTenantId?: string,
|
||||
dkimSelector: string = "usesend",
|
||||
) {
|
||||
const sesClient = getSesClient(region);
|
||||
|
||||
@@ -93,7 +94,7 @@ export async function addDomain(
|
||||
const command = new CreateEmailIdentityCommand({
|
||||
EmailIdentity: domain,
|
||||
DkimSigningAttributes: {
|
||||
DomainSigningSelector: "unsend",
|
||||
DomainSigningSelector: dkimSelector,
|
||||
DomainSigningPrivateKey: privateKey,
|
||||
},
|
||||
});
|
||||
@@ -114,13 +115,13 @@ export async function addDomain(
|
||||
});
|
||||
|
||||
const tenantResourceAssociationResponse = await sesClient.send(
|
||||
tenantResourceAssociationCommand
|
||||
tenantResourceAssociationCommand,
|
||||
);
|
||||
|
||||
if (tenantResourceAssociationResponse.$metadata.httpStatusCode !== 200) {
|
||||
logger.error(
|
||||
{ tenantResourceAssociationResponse },
|
||||
"Failed to associate domain with tenant"
|
||||
"Failed to associate domain with tenant",
|
||||
);
|
||||
throw new Error("Failed to associate domain with tenant");
|
||||
}
|
||||
@@ -132,7 +133,7 @@ export async function addDomain(
|
||||
) {
|
||||
logger.error(
|
||||
{ response, emailIdentityResponse },
|
||||
"Failed to create domain identity"
|
||||
"Failed to create domain identity",
|
||||
);
|
||||
throw new Error("Failed to create domain identity");
|
||||
}
|
||||
@@ -143,7 +144,7 @@ export async function addDomain(
|
||||
export async function deleteDomain(
|
||||
domain: string,
|
||||
region: string,
|
||||
sesTenantId?: string
|
||||
sesTenantId?: string,
|
||||
) {
|
||||
const sesClient = getSesClient(region);
|
||||
|
||||
@@ -155,13 +156,13 @@ export async function deleteDomain(
|
||||
});
|
||||
|
||||
const tenantResourceAssociationResponse = await sesClient.send(
|
||||
tenantResourceAssociationCommand
|
||||
tenantResourceAssociationCommand,
|
||||
);
|
||||
|
||||
if (tenantResourceAssociationResponse.$metadata.httpStatusCode !== 200) {
|
||||
logger.error(
|
||||
{ tenantResourceAssociationResponse },
|
||||
"Failed to delete tenant resource association"
|
||||
"Failed to delete tenant resource association",
|
||||
);
|
||||
throw new Error("Failed to delete tenant resource association");
|
||||
}
|
||||
@@ -233,7 +234,9 @@ export async function sendRawEmail({
|
||||
bcc,
|
||||
headers: {
|
||||
"X-Entity-Ref-ID": nanoid(),
|
||||
...(emailId ? { "X-Unsend-Email-ID": emailId } : {}),
|
||||
...(emailId
|
||||
? { "X-Usesend-Email-ID": emailId, "X-Unsend-Email-ID": emailId }
|
||||
: {}),
|
||||
...(unsubUrl
|
||||
? {
|
||||
"List-Unsubscribe": `<${unsubUrl}>`,
|
||||
@@ -289,7 +292,7 @@ export async function addWebhookConfiguration(
|
||||
configName: string,
|
||||
topicArn: string,
|
||||
eventTypes: EventType[],
|
||||
region: string
|
||||
region: string,
|
||||
) {
|
||||
const sesClient = getSesClient(region);
|
||||
|
||||
@@ -305,7 +308,7 @@ export async function addWebhookConfiguration(
|
||||
|
||||
const command = new CreateConfigurationSetEventDestinationCommand({
|
||||
ConfigurationSetName: configName, // required
|
||||
EventDestinationName: "unsend_destination", // required
|
||||
EventDestinationName: "usesend_destination", // required
|
||||
EventDestination: {
|
||||
Enabled: true,
|
||||
MatchingEventTypes: eventTypes,
|
||||
|
@@ -16,7 +16,7 @@ interface OtpEmailProps {
|
||||
export function OtpEmail({
|
||||
otpCode,
|
||||
loginUrl,
|
||||
hostName = "Unsend",
|
||||
hostName = "useSend",
|
||||
logoUrl,
|
||||
}: OtpEmailProps) {
|
||||
return (
|
||||
@@ -45,7 +45,7 @@ export function OtpEmail({
|
||||
textAlign: "left" as const,
|
||||
}}
|
||||
>
|
||||
Use the verification code below to sign in to your Unsend account:
|
||||
Use the verification code below to sign in to your useSend account:
|
||||
</Text>
|
||||
|
||||
<Container
|
||||
|
@@ -22,7 +22,7 @@ export function TeamInviteEmail({
|
||||
role = "member",
|
||||
}: TeamInviteEmailProps) {
|
||||
return (
|
||||
<EmailLayout preview={`You've been invited to join ${teamName} on Unsend`}>
|
||||
<EmailLayout preview={`You've been invited to join ${teamName} on useSend`}>
|
||||
<EmailHeader logoUrl={logoUrl} title="You're invited!" />
|
||||
|
||||
<Container style={{ padding: "20px 0", textAlign: "left" as const }}>
|
||||
@@ -50,7 +50,7 @@ export function TeamInviteEmail({
|
||||
{inviterName
|
||||
? `${inviterName} has invited you to join `
|
||||
: "You have been invited to join "}
|
||||
<strong style={{ color: "#000000" }}>{teamName}</strong> on Unsend
|
||||
<strong style={{ color: "#000000" }}>{teamName}</strong> on useSend
|
||||
{role && role !== "member" && (
|
||||
<span>
|
||||
{" "}
|
||||
|
@@ -7,8 +7,8 @@ interface EmailFooterProps {
|
||||
}
|
||||
|
||||
export function EmailFooter({
|
||||
companyName = "Unsend",
|
||||
supportUrl = "https://unsend.dev"
|
||||
companyName = "useSend",
|
||||
supportUrl = "https://usesend.com"
|
||||
}: EmailFooterProps) {
|
||||
return (
|
||||
<Container
|
||||
@@ -40,4 +40,4 @@ export function EmailFooter({
|
||||
</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@ export function EmailHeader({ logoUrl, title }: EmailHeaderProps) {
|
||||
{logoUrl && (
|
||||
<Img
|
||||
src={logoUrl}
|
||||
alt="Unsend"
|
||||
alt="useSend"
|
||||
style={{
|
||||
width: "48px",
|
||||
height: "48px",
|
||||
@@ -42,4 +42,4 @@ export function EmailHeader({ logoUrl, title }: EmailHeaderProps) {
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -7,8 +7,8 @@ async function testEmailTemplates() {
|
||||
// Test OTP email
|
||||
const otpHtml = await renderOtpEmail({
|
||||
otpCode: 'ABC123',
|
||||
loginUrl: 'https://app.unsend.dev/login?token=abc123',
|
||||
hostName: 'Unsend',
|
||||
loginUrl: 'https://app.usesend.com/login?token=abc123',
|
||||
hostName: 'useSend',
|
||||
});
|
||||
|
||||
console.log('✅ OTP Email rendered successfully');
|
||||
@@ -17,7 +17,7 @@ async function testEmailTemplates() {
|
||||
// Test Team Invite email
|
||||
const inviteHtml = await renderTeamInviteEmail({
|
||||
teamName: 'My Awesome Team',
|
||||
inviteUrl: 'https://app.unsend.dev/join-team?inviteId=123',
|
||||
inviteUrl: 'https://app.usesend.com/join-team?inviteId=123',
|
||||
});
|
||||
|
||||
console.log('✅ Team Invite Email rendered successfully');
|
||||
@@ -33,4 +33,4 @@ async function testEmailTemplates() {
|
||||
|
||||
if (require.main === module) {
|
||||
testEmailTemplates();
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { env } from "~/env";
|
||||
import { Unsend } from "unsend";
|
||||
import { UseSend } from "usesend";
|
||||
import { isSelfHosted } from "~/utils/common";
|
||||
import { db } from "./db";
|
||||
import { getDomains } from "./service/domain-service";
|
||||
@@ -7,13 +7,13 @@ import { sendEmail } from "./service/email-service";
|
||||
import { logger } from "./logger/log";
|
||||
import { renderOtpEmail, renderTeamInviteEmail } from "./email-templates";
|
||||
|
||||
let unsend: Unsend | undefined;
|
||||
let usesend: UseSend | undefined;
|
||||
|
||||
const getClient = () => {
|
||||
if (!unsend) {
|
||||
unsend = new Unsend(env.UNSEND_API_KEY);
|
||||
if (!usesend) {
|
||||
usesend = new UseSend(env.USESEND_API_KEY ?? env.UNSEND_API_KEY);
|
||||
}
|
||||
return unsend;
|
||||
return usesend;
|
||||
};
|
||||
|
||||
export async function sendSignUpEmail(
|
||||
@@ -28,7 +28,7 @@ export async function sendSignUpEmail(
|
||||
return;
|
||||
}
|
||||
|
||||
const subject = "Sign in to Unsend";
|
||||
const subject = "Sign in to useSend";
|
||||
|
||||
// Use jsx-email template for beautiful HTML
|
||||
const html = await renderOtpEmail({
|
||||
@@ -38,7 +38,7 @@ export async function sendSignUpEmail(
|
||||
});
|
||||
|
||||
// Fallback text version
|
||||
const text = `Hey,\n\nYou can sign in to Unsend by clicking the below URL:\n${url}\n\nYou can also use this OTP: ${token}\n\nThanks,\nUnsend Team`;
|
||||
const text = `Hey,\n\nYou can sign in to useSend by clicking the below URL:\n${url}\n\nYou can also use this OTP: ${token}\n\nThanks,\nuseSend Team`;
|
||||
|
||||
await sendMail(email, subject, text, html);
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export async function sendTeamInviteEmail(
|
||||
return;
|
||||
}
|
||||
|
||||
const subject = "You have been invited to join Unsend";
|
||||
const subject = "You have been invited to join useSend";
|
||||
|
||||
// Use jsx-email template for beautiful HTML
|
||||
const html = await renderTeamInviteEmail({
|
||||
@@ -64,7 +64,7 @@ export async function sendTeamInviteEmail(
|
||||
});
|
||||
|
||||
// Fallback text version
|
||||
const text = `Hey,\n\nYou have been invited to join the team ${teamName} on Unsend.\n\nYou can accept the invitation by clicking the below URL:\n${url}\n\nThanks,\nUnsend Team`;
|
||||
const text = `Hey,\n\nYou have been invited to join the team ${teamName} on useSend.\n\nYou can accept the invitation by clicking the below URL:\n${url}\n\nThanks,\nuseSend Team`;
|
||||
|
||||
await sendMail(email, subject, text, html);
|
||||
}
|
||||
@@ -118,15 +118,15 @@ async function sendMail(
|
||||
});
|
||||
|
||||
if (resp.data) {
|
||||
logger.info("Email sent using unsend");
|
||||
logger.info("Email sent using usesend");
|
||||
return;
|
||||
} else {
|
||||
logger.error(
|
||||
{ code: resp.error?.code, message: resp.error?.message },
|
||||
"Error sending email using unsend, so fallback to resend"
|
||||
"Error sending email using usesend, so fallback to resend"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error("UNSEND_API_KEY not found");
|
||||
throw new Error("USESEND_API_KEY/UNSEND_API_KEY not found");
|
||||
}
|
||||
}
|
||||
|
@@ -118,7 +118,7 @@ export function getApp() {
|
||||
openapi: "3.0.0",
|
||||
info: {
|
||||
version: "1.0.0",
|
||||
title: "Unsend API",
|
||||
title: "useSend API",
|
||||
},
|
||||
servers: [{ url: `${env.NEXTAUTH_URL}/api` }],
|
||||
}));
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { EmailRenderer } from "@unsend/email-editor/src/renderer";
|
||||
import { EmailRenderer } from "@usesend/email-editor/src/renderer";
|
||||
import { db } from "../db";
|
||||
import { createHash } from "crypto";
|
||||
import { env } from "~/env";
|
||||
@@ -263,7 +263,10 @@ async function processContactEmail(jobData: CampaignEmailJob) {
|
||||
const renderer = new EmailRenderer(jsonContent);
|
||||
|
||||
const unsubscribeUrl = createUnsubUrl(contact.id, emailConfig.campaignId);
|
||||
const oneClickUnsubUrl = createOneClickUnsubUrl(contact.id, emailConfig.campaignId);
|
||||
const oneClickUnsubUrl = createOneClickUnsubUrl(
|
||||
contact.id,
|
||||
emailConfig.campaignId
|
||||
);
|
||||
|
||||
// Check for suppressed emails before processing
|
||||
const toEmails = [contact.email];
|
||||
@@ -303,6 +306,7 @@ async function processContactEmail(jobData: CampaignEmailJob) {
|
||||
},
|
||||
linkValues: {
|
||||
"{{unsend_unsubscribe_url}}": unsubscribeUrl,
|
||||
"{{usesend_unsubscribe_url}}": unsubscribeUrl,
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -41,7 +41,7 @@ export async function validateDomainFromEmail(email: string, teamId: number) {
|
||||
if (!domain) {
|
||||
throw new UnsendApiError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Domain: ${fromDomain} of from email is wrong. Use the domain verified by unsend`,
|
||||
message: `Domain: ${fromDomain} of from email is wrong. Use the domain verified by useSend`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -86,7 +86,8 @@ export async function createDomain(
|
||||
}
|
||||
|
||||
const subdomain = tldts.getSubdomain(name);
|
||||
const publicKey = await ses.addDomain(name, region, sesTenantId);
|
||||
const dkimSelector = "usesend";
|
||||
const publicKey = await ses.addDomain(name, region, sesTenantId, dkimSelector);
|
||||
|
||||
const domain = await db.domain.create({
|
||||
data: {
|
||||
@@ -96,6 +97,7 @@ export async function createDomain(
|
||||
subdomain,
|
||||
region,
|
||||
sesTenantId,
|
||||
dkimSelector,
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -3,7 +3,7 @@ import { db } from "../db";
|
||||
import { UnsendApiError } from "~/server/public-api/api-error";
|
||||
import { EmailQueueService } from "./email-queue-service";
|
||||
import { validateDomainFromEmail } from "./domain-service";
|
||||
import { EmailRenderer } from "@unsend/email-editor/src/renderer";
|
||||
import { EmailRenderer } from "@usesend/email-editor/src/renderer";
|
||||
import { logger } from "../logger/log";
|
||||
import { SuppressionService } from "./suppression-service";
|
||||
|
||||
@@ -35,7 +35,7 @@ async function checkIfValidEmail(emailId: string) {
|
||||
|
||||
export const replaceVariables = (
|
||||
text: string,
|
||||
variables: Record<string, string>
|
||||
variables: Record<string, string>,
|
||||
) => {
|
||||
return Object.keys(variables).reduce((accum, key) => {
|
||||
const re = new RegExp(`{{${key}}}`, "g");
|
||||
@@ -48,7 +48,7 @@ export const replaceVariables = (
|
||||
Send transactional email
|
||||
*/
|
||||
export async function sendEmail(
|
||||
emailContent: EmailContent & { teamId: number; apiKeyId?: number }
|
||||
emailContent: EmailContent & { teamId: number; apiKeyId?: number },
|
||||
) {
|
||||
const {
|
||||
to,
|
||||
@@ -84,18 +84,18 @@ export async function sendEmail(
|
||||
|
||||
const suppressionResults = await SuppressionService.checkMultipleEmails(
|
||||
allEmailsToCheck,
|
||||
teamId
|
||||
teamId,
|
||||
);
|
||||
|
||||
// Filter each field separately
|
||||
const filteredToEmails = toEmails.filter(
|
||||
(email) => !suppressionResults[email]
|
||||
(email) => !suppressionResults[email],
|
||||
);
|
||||
const filteredCcEmails = ccEmails.filter(
|
||||
(email) => !suppressionResults[email]
|
||||
(email) => !suppressionResults[email],
|
||||
);
|
||||
const filteredBccEmails = bccEmails.filter(
|
||||
(email) => !suppressionResults[email]
|
||||
(email) => !suppressionResults[email],
|
||||
);
|
||||
|
||||
// Only block the email if all TO recipients are suppressed
|
||||
@@ -105,7 +105,7 @@ export async function sendEmail(
|
||||
to,
|
||||
teamId,
|
||||
},
|
||||
"All TO recipients are suppressed. No emails to send."
|
||||
"All TO recipients are suppressed. No emails to send.",
|
||||
);
|
||||
|
||||
const email = await db.email.create({
|
||||
@@ -147,7 +147,7 @@ export async function sendEmail(
|
||||
filteredCc: filteredCcEmails,
|
||||
teamId,
|
||||
},
|
||||
"Some CC recipients were suppressed and filtered out."
|
||||
"Some CC recipients were suppressed and filtered out.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ export async function sendEmail(
|
||||
filteredBcc: filteredBccEmails,
|
||||
teamId,
|
||||
},
|
||||
"Some BCC recipients were suppressed and filtered out."
|
||||
"Some BCC recipients were suppressed and filtered out.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ export async function sendEmail(
|
||||
acc[`{{${key}}}`] = variables?.[key] || "";
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
{} as Record<string, string>,
|
||||
),
|
||||
};
|
||||
|
||||
@@ -251,7 +251,7 @@ export async function sendEmail(
|
||||
domain.region,
|
||||
true,
|
||||
undefined,
|
||||
delay
|
||||
delay,
|
||||
);
|
||||
} catch (error: any) {
|
||||
await db.emailEvent.create({
|
||||
@@ -280,7 +280,7 @@ export async function updateEmail(
|
||||
scheduledAt,
|
||||
}: {
|
||||
scheduledAt?: string;
|
||||
}
|
||||
},
|
||||
) {
|
||||
const { email, domain } = await checkIfValidEmail(emailId);
|
||||
|
||||
@@ -344,7 +344,7 @@ export async function sendBulkEmails(
|
||||
teamId: number;
|
||||
apiKeyId?: number;
|
||||
}
|
||||
>
|
||||
>,
|
||||
) {
|
||||
if (emailContents.length === 0) {
|
||||
throw new UnsendApiError({
|
||||
@@ -382,18 +382,18 @@ export async function sendBulkEmails(
|
||||
|
||||
const suppressionResults = await SuppressionService.checkMultipleEmails(
|
||||
allEmailsToCheck,
|
||||
content.teamId
|
||||
content.teamId,
|
||||
);
|
||||
|
||||
// Filter each field separately
|
||||
const filteredToEmails = toEmails.filter(
|
||||
(email) => !suppressionResults[email]
|
||||
(email) => !suppressionResults[email],
|
||||
);
|
||||
const filteredCcEmails = ccEmails.filter(
|
||||
(email) => !suppressionResults[email]
|
||||
(email) => !suppressionResults[email],
|
||||
);
|
||||
const filteredBccEmails = bccEmails.filter(
|
||||
(email) => !suppressionResults[email]
|
||||
(email) => !suppressionResults[email],
|
||||
);
|
||||
|
||||
// Only consider it suppressed if all TO recipients are suppressed
|
||||
@@ -410,13 +410,13 @@ export async function sendBulkEmails(
|
||||
suppressed: hasSuppressedToEmails,
|
||||
suppressedEmails: toEmails.filter((email) => suppressionResults[email]),
|
||||
suppressedCcEmails: ccEmails.filter(
|
||||
(email) => suppressionResults[email]
|
||||
(email) => suppressionResults[email],
|
||||
),
|
||||
suppressedBccEmails: bccEmails.filter(
|
||||
(email) => suppressionResults[email]
|
||||
(email) => suppressionResults[email],
|
||||
),
|
||||
};
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const validEmails = emailChecks.filter((check) => !check.suppressed);
|
||||
@@ -433,7 +433,7 @@ export async function sendBulkEmails(
|
||||
suppressedAddresses: info.suppressedEmails,
|
||||
})),
|
||||
},
|
||||
"Filtered suppressed emails from bulk send"
|
||||
"Filtered suppressed emails from bulk send",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -490,7 +490,7 @@ export async function sendBulkEmails(
|
||||
acc[`{{${key}}}`] = variables?.[key] || "";
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
{} as Record<string, string>,
|
||||
),
|
||||
};
|
||||
|
||||
@@ -647,7 +647,7 @@ export async function sendBulkEmails(
|
||||
acc[`{{${key}}}`] = variables?.[key] || "";
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
{} as Record<string, string>,
|
||||
),
|
||||
};
|
||||
|
||||
@@ -709,7 +709,7 @@ export async function sendBulkEmails(
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
{ err: error, to },
|
||||
`Failed to create email record for recipient`
|
||||
`Failed to create email record for recipient`,
|
||||
);
|
||||
// Continue processing other emails
|
||||
}
|
||||
@@ -744,7 +744,7 @@ export async function sendBulkEmails(
|
||||
where: { id: email.email.id },
|
||||
data: { latestStatus: "FAILED" },
|
||||
});
|
||||
})
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
@@ -53,7 +53,7 @@ export async function parseSesHook(data: SesEvent) {
|
||||
// Handle race condition: If email not found by sesEmailId, try to find by custom header
|
||||
if (!email) {
|
||||
const emailIdHeader = data.mail.headers.find(
|
||||
(h) => h.name === "X-Unsend-Email-ID"
|
||||
(h) => h.name === "X-Usesend-Email-ID" || h.name === "X-Unsend-Email-ID",
|
||||
);
|
||||
|
||||
if (emailIdHeader?.value) {
|
||||
@@ -71,7 +71,7 @@ export async function parseSesHook(data: SesEvent) {
|
||||
});
|
||||
logger.info(
|
||||
{ emailId: email.id, sesEmailId },
|
||||
"Updated email with sesEmailId from webhook (race condition resolved)"
|
||||
"Updated email with sesEmailId from webhook (race condition resolved)",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -131,8 +131,8 @@ export async function parseSesHook(data: SesEvent) {
|
||||
? SuppressionReason.HARD_BOUNCE
|
||||
: SuppressionReason.COMPLAINT,
|
||||
source: email.id,
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
logger.info(
|
||||
@@ -141,7 +141,7 @@ export async function parseSesHook(data: SesEvent) {
|
||||
recipients: recipientEmails,
|
||||
reason: isHardBounced ? "HARD_BOUNCE" : "COMPLAINT",
|
||||
},
|
||||
"Added emails to suppression list due to bounce/complaint"
|
||||
"Added emails to suppression list due to bounce/complaint",
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
@@ -150,7 +150,7 @@ export async function parseSesHook(data: SesEvent) {
|
||||
recipients: recipientEmails,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
"Failed to add emails to suppression list"
|
||||
"Failed to add emails to suppression list",
|
||||
);
|
||||
// Don't throw error - continue processing the webhook
|
||||
}
|
||||
@@ -251,7 +251,7 @@ export async function parseSesHook(data: SesEvent) {
|
||||
await updateCampaignAnalytics(
|
||||
email.campaignId,
|
||||
mailStatus,
|
||||
isHardBounced
|
||||
isHardBounced,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -334,7 +334,7 @@ async function checkUnsubscribe({
|
||||
event === EmailStatus.BOUNCED
|
||||
? UnsubscribeReason.BOUNCED
|
||||
: UnsubscribeReason.COMPLAINED,
|
||||
})
|
||||
}),
|
||||
),
|
||||
]);
|
||||
}
|
||||
@@ -390,13 +390,13 @@ export class SesHookParser {
|
||||
}),
|
||||
async () => {
|
||||
await this.execute(job.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
{
|
||||
connection: getRedis(),
|
||||
concurrency: 50,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
private static async execute(event: SesEvent) {
|
||||
@@ -412,7 +412,7 @@ export class SesHookParser {
|
||||
return await this.sesHookQueue.add(
|
||||
data.messageId,
|
||||
data.event,
|
||||
DEFAULT_QUEUE_OPTIONS
|
||||
DEFAULT_QUEUE_OPTIONS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@ export class SesSettingsService {
|
||||
private static initialized = false;
|
||||
|
||||
public static async getSetting(
|
||||
region = env.AWS_DEFAULT_REGION
|
||||
region = env.AWS_DEFAULT_REGION,
|
||||
): Promise<SesSetting | null> {
|
||||
await this.checkInitialized();
|
||||
|
||||
@@ -46,19 +46,19 @@ export class SesSettingsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new setting in AWS for the given region and unsendUrl
|
||||
* Creates a new setting in AWS for the given region and usesendUrl
|
||||
*
|
||||
* @param region
|
||||
* @param unsendUrl
|
||||
* @param usesendUrl
|
||||
*/
|
||||
public static async createSesSetting({
|
||||
region,
|
||||
unsendUrl,
|
||||
usesendUrl,
|
||||
sendingRateLimit,
|
||||
transactionalQuota,
|
||||
}: {
|
||||
region: string;
|
||||
unsendUrl: string;
|
||||
usesendUrl: string;
|
||||
sendingRateLimit: number;
|
||||
transactionalQuota: number;
|
||||
}) {
|
||||
@@ -67,15 +67,15 @@ export class SesSettingsService {
|
||||
throw new Error(`SesSetting for region ${region} already exists`);
|
||||
}
|
||||
|
||||
const parsedUrl = unsendUrl.endsWith("/")
|
||||
? unsendUrl.substring(0, unsendUrl.length - 1)
|
||||
: unsendUrl;
|
||||
const parsedUrl = usesendUrl.endsWith("/")
|
||||
? usesendUrl.substring(0, usesendUrl.length - 1)
|
||||
: usesendUrl;
|
||||
|
||||
const unsendUrlValidation = await isValidUnsendUrl(parsedUrl);
|
||||
const usesendUrlValidation = await isValidUsesendUrl(parsedUrl);
|
||||
|
||||
if (!unsendUrlValidation.isValid) {
|
||||
if (!usesendUrlValidation.isValid) {
|
||||
throw new Error(
|
||||
`Unsend URL: ${unsendUrl} is not valid, status: ${unsendUrlValidation.code} message:${unsendUrlValidation.error}`
|
||||
`Callback URL: ${usesendUrl} is not valid, status: ${usesendUrlValidation.code} message:${usesendUrlValidation.error}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ export class SesSettingsService {
|
||||
await sns.subscribeEndpoint(
|
||||
topicArn!,
|
||||
`${setting.callbackUrl}`,
|
||||
setting.region
|
||||
setting.region,
|
||||
);
|
||||
|
||||
return setting;
|
||||
@@ -120,14 +120,14 @@ export class SesSettingsService {
|
||||
EmailQueueService.initializeQueue(
|
||||
region,
|
||||
setting.sesEmailRateLimit,
|
||||
setting.transactionalQuota
|
||||
setting.transactionalQuota,
|
||||
);
|
||||
logger.info(
|
||||
{
|
||||
transactionalQueue: EmailQueueService.transactionalQueue,
|
||||
marketingQueue: EmailQueueService.marketingQueue,
|
||||
},
|
||||
"Email queues initialized"
|
||||
"Email queues initialized",
|
||||
);
|
||||
|
||||
await this.invalidateCache();
|
||||
@@ -138,7 +138,7 @@ export class SesSettingsService {
|
||||
} catch (deleteError) {
|
||||
logger.error(
|
||||
{ err: deleteError },
|
||||
"Failed to delete SNS topic after error"
|
||||
"Failed to delete SNS topic after error",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -172,13 +172,13 @@ export class SesSettingsService {
|
||||
transactionalQueue: EmailQueueService.transactionalQueue,
|
||||
marketingQueue: EmailQueueService.marketingQueue,
|
||||
},
|
||||
"Email queues before update"
|
||||
"Email queues before update",
|
||||
);
|
||||
|
||||
EmailQueueService.initializeQueue(
|
||||
setting.region,
|
||||
setting.sesEmailRateLimit,
|
||||
setting.transactionalQuota
|
||||
setting.transactionalQuota,
|
||||
);
|
||||
|
||||
logger.info(
|
||||
@@ -186,7 +186,7 @@ export class SesSettingsService {
|
||||
transactionalQueue: EmailQueueService.transactionalQueue,
|
||||
marketingQueue: EmailQueueService.marketingQueue,
|
||||
},
|
||||
"Email queues after update"
|
||||
"Email queues after update",
|
||||
);
|
||||
|
||||
await this.invalidateCache();
|
||||
@@ -229,7 +229,7 @@ async function registerConfigurationSet(setting: SesSetting) {
|
||||
configGeneral,
|
||||
setting.topicArn,
|
||||
GENERAL_EVENTS,
|
||||
setting.region
|
||||
setting.region,
|
||||
);
|
||||
|
||||
const configClick = `${setting.idPrefix}-${setting.region}-unsend-click`;
|
||||
@@ -237,7 +237,7 @@ async function registerConfigurationSet(setting: SesSetting) {
|
||||
configClick,
|
||||
setting.topicArn,
|
||||
[...GENERAL_EVENTS, "CLICK"],
|
||||
setting.region
|
||||
setting.region,
|
||||
);
|
||||
|
||||
const configOpen = `${setting.idPrefix}-${setting.region}-unsend-open`;
|
||||
@@ -245,7 +245,7 @@ async function registerConfigurationSet(setting: SesSetting) {
|
||||
configOpen,
|
||||
setting.topicArn,
|
||||
[...GENERAL_EVENTS, "OPEN"],
|
||||
setting.region
|
||||
setting.region,
|
||||
);
|
||||
|
||||
const configFull = `${setting.idPrefix}-${setting.region}-unsend-full`;
|
||||
@@ -253,7 +253,7 @@ async function registerConfigurationSet(setting: SesSetting) {
|
||||
configFull,
|
||||
setting.topicArn,
|
||||
[...GENERAL_EVENTS, "CLICK", "OPEN"],
|
||||
setting.region
|
||||
setting.region,
|
||||
);
|
||||
|
||||
return await db.sesSetting.update({
|
||||
@@ -273,7 +273,7 @@ async function registerConfigurationSet(setting: SesSetting) {
|
||||
});
|
||||
}
|
||||
|
||||
async function isValidUnsendUrl(url: string) {
|
||||
async function isValidUsesendUrl(url: string) {
|
||||
logger.info({ url }, "Checking if URL is valid");
|
||||
try {
|
||||
const response = await fetch(`${url}/api/ses_callback`, {
|
||||
|
Reference in New Issue
Block a user