rebrand to useSend (#210)

This commit is contained in:
KM Koushik
2025-09-03 08:21:55 +10:00
committed by GitHub
parent b1a59d2705
commit 07c53d3f58
219 changed files with 1349 additions and 2835 deletions

View File

@@ -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";

View File

@@ -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({

View File

@@ -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">

View File

@@ -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();

View File

@@ -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);
}}

View File

@@ -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">

View File

@@ -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");

View File

@@ -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, {

View File

@@ -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({

View File

@@ -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";

View File

@@ -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 />

View File

@@ -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, {

View File

@@ -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) {

View File

@@ -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({

View File

@@ -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({

View File

@@ -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: () => {

View File

@@ -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";

View File

@@ -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() {

View File

@@ -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({

View File

@@ -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" }),

View File

@@ -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 />

View File

@@ -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 }) {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -1,4 +1,4 @@
import { useTheme } from "@unsend/ui";
import { useTheme } from "@usesend/ui";
export function useColors() {
const { resolvedTheme } = useTheme();

View File

@@ -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}

View File

@@ -3,7 +3,7 @@ import {
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@unsend/ui/src/tooltip";
} from "@usesend/ui/src/tooltip";
import {
CheckCircle2,
CheckCircle2Icon,

View File

@@ -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, {

View File

@@ -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();

View File

@@ -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(),

View File

@@ -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 />

View File

@@ -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 />

View File

@@ -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";

View File

@@ -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");
},
}
},
);
}

View File

@@ -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 (

View File

@@ -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>
);
};

View File

@@ -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

View File

@@ -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();

View File

@@ -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 />

View File

@@ -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(),

View File

@@ -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;

View File

@@ -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";

View File

@@ -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 },
);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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";

View File

@@ -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<{

View File

@@ -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";

View File

@@ -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"]),

View File

@@ -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";

View File

@@ -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<{

View File

@@ -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";

View File

@@ -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">

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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" />

View File

@@ -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;

View File

@@ -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",

View File

@@ -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";

View File

@@ -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, {

View File

@@ -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({

View File

@@ -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";

View File

@@ -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 />

View File

@@ -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";

View File

@@ -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",
});

View File

@@ -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",
},
}
},
);
}
}

View File

@@ -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";

View File

@@ -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>

View File

@@ -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"

View File

@@ -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>

View File

@@ -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";

View File

@@ -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>

View File

@@ -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"

View File

@@ -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 = () => {

View File

@@ -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 = () => {

View File

@@ -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";

View File

@@ -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>

View File

@@ -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({

View File

@@ -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";

View File

@@ -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")}
>

View File

@@ -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,

View File

@@ -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}"}'`,

View File

@@ -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.",
},
};

View File

@@ -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,
);
}),
});

View File

@@ -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}`;

View File

@@ -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}`;

View File

@@ -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,

View File

@@ -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

View File

@@ -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>
{" "}

View File

@@ -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>
);
}
}

View File

@@ -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>
);
}
}

View File

@@ -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();
}
}

View File

@@ -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");
}
}

View File

@@ -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` }],
}));

View File

@@ -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,
},
});

View File

@@ -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,
},
});

View File

@@ -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;
}

View File

@@ -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,
);
}
}

View File

@@ -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`, {