diff --git a/apps/marketing/src/app/layout.tsx b/apps/marketing/src/app/layout.tsx
index 9ec174b..fa66b1e 100644
--- a/apps/marketing/src/app/layout.tsx
+++ b/apps/marketing/src/app/layout.tsx
@@ -1,6 +1,6 @@
+import "@unsend/ui/styles/globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
-import "@unsend/ui/styles/globals.css";
import { ThemeProvider } from "@unsend/ui/theme-provider";
const inter = Inter({ subsets: ["latin"] });
diff --git a/apps/marketing/src/app/page.tsx b/apps/marketing/src/app/page.tsx
index a457384..2f5f9ac 100644
--- a/apps/marketing/src/app/page.tsx
+++ b/apps/marketing/src/app/page.tsx
@@ -1,4 +1,4 @@
-"use client";
+"use client"
import { motion } from "framer-motion";
import {
@@ -7,15 +7,123 @@ import {
MegaphoneIcon,
ChatBubbleOvalLeftEllipsisIcon,
BellAlertIcon,
+ DevicePhoneMobileIcon,
} from "@heroicons/react/24/solid";
+import {
+ Heading1,
+ Heading2,
+ Heading3,
+ AlignLeft,
+ AlignRight,
+ AlignCenter,
+ Bold,
+ Italic,
+ ListOrdered,
+} from "lucide-react";
import { formatDate } from "date-fns";
+import { Code } from "@unsend/ui/src/code"
+import { hi } from "date-fns/locale";
+
+
+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": "
Hello this is a test mail
"
+ }),
+ 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": "Hello this is a test mail
"
+})
+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" => "Hello this is a test mail
"
+})
+
+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" => "Hello this is a test mail
"
+));
+
+$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;
+}`;
+
export default function Home() {
return (
-
-
-
+
+
+
Open source sending infrastructure for{" "}
developers
@@ -42,12 +150,12 @@ export default function Home() {
{/* */}
-
+
-
Reach your users
+
Reach your users
-
-
+
+
Transactional Mail
@@ -58,7 +166,7 @@ export default function Home() {
Get notified of email bounces and complaints.
-
+
-
-
+
+
- Manage newsletters, changelogs, and broadcasts easily.
- - Use our no-code email builder and templates.
+ -
+ Use our no-code email builder and templates that works on all
+ email clients.
+
- Measure engagement using click and open tracking.
- - We will manage subscriptions for you.
+ -
+ Focus on the content and we will handle the subscription for
+ you.
+
-
-
-
-
-
-
-
-
SMS
-
- Coming soon
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- - Manage newsletters, changelogs, and broadcasts easily.
- - Use our no-code email builder and templates.
- - Measure engagement using click and open tracking.
- - We will manage subscriptions for you.
-
-
-
-
-
-
-
-
-
-
Push notification
-
- Coming soon
+
+
+
Welcome to unsend!
+
+ Finally an open source alternative for Resend, Mailgun,
+ Sendgrid and postmark.
+
-
-
-
- - Manage newsletters, changelogs, and broadcasts easily.
- - Use our no-code email builder and templates.
- - Measure engagement using click and open tracking.
- - We will manage subscriptions for you.
-
+
+
-
+
+
+
+
+
SMS & Push notification
+
+
+
+
+
+ {'Coming soon!'.split('').map((l, i) => (
+ {l}
+ ))}
+
+
+
+
+
Integrate in minutes
+
+
+
+
+
+
diff --git a/apps/marketing/tailwind.config.ts b/apps/marketing/tailwind.config.ts
index 37212a9..1428d5e 100644
--- a/apps/marketing/tailwind.config.ts
+++ b/apps/marketing/tailwind.config.ts
@@ -4,5 +4,8 @@ import path from "path";
export default {
...sharedConfig,
- content: ["./src/**/*.tsx"],
+ content: [
+ "./src/**/*.tsx",
+ `${path.join(require.resolve("@unsend/ui"), "..")}/**/*.{ts,tsx}`,
+ ],
} satisfies Config;
diff --git a/apps/web/src/app/(dashboard)/domains/[domainId]/send-test-mail.tsx b/apps/web/src/app/(dashboard)/domains/[domainId]/send-test-mail.tsx
index 3d45ac5..01afffb 100644
--- a/apps/web/src/app/(dashboard)/domains/[domainId]/send-test-mail.tsx
+++ b/apps/web/src/app/(dashboard)/domains/[domainId]/send-test-mail.tsx
@@ -151,6 +151,7 @@ export const SendTestMail: React.FC<{ domain: Domain }> = ({ domain }) => {
{ language: "php", code: phpCode },
{ language: "python", code: pythonCode },
]}
+ codeClassName="max-w-[38rem] h-[20rem]"
/>
-
+
diff --git a/apps/web/src/app/(dashboard)/emails/email-status-badge.tsx b/apps/web/src/app/(dashboard)/emails/email-status-badge.tsx
index 31e0d7f..674c544 100644
--- a/apps/web/src/app/(dashboard)/emails/email-status-badge.tsx
+++ b/apps/web/src/app/(dashboard)/emails/email-status-badge.tsx
@@ -12,7 +12,7 @@ export const EmailStatusBadge: React.FC<{ status: EmailStatus }> = ({
badgeColor = "bg-emerald-500/10 text-emerald-500 border-emerald-600/10";
break;
case "BOUNCED":
- badgeColor = "bg-red-500/10 text-red-800 border-red-600/10";
+ badgeColor = "bg-red-500/10 text-red-600 border-red-600/10";
break;
case "CLICKED":
badgeColor = "bg-cyan-500/10 text-cyan-600 border-cyan-600/10";
@@ -47,27 +47,27 @@ export const EmailStatusIcon: React.FC<{ status: EmailStatus }> = ({
switch (status) {
case "DELIVERED":
- outsideColor = "bg-emerald-500/40";
+ outsideColor = "bg-emerald-500/30";
insideColor = "bg-emerald-500";
break;
case "BOUNCED":
- outsideColor = "bg-red-500/40";
+ outsideColor = "bg-red-500/30";
insideColor = "bg-red-500";
break;
case "CLICKED":
- outsideColor = "bg-cyan-500/40";
+ outsideColor = "bg-cyan-500/30";
insideColor = "bg-cyan-500";
break;
case "OPENED":
- outsideColor = "bg-indigo-500/40";
+ outsideColor = "bg-indigo-500/30";
insideColor = "bg-indigo-500";
break;
case "DELIVERY_DELAYED":
- outsideColor = "bg-yellow-500/40";
+ outsideColor = "bg-yellow-500/30";
insideColor = "bg-yellow-500";
break;
case "COMPLAINED":
- outsideColor = "bg-yellow-500/40";
+ outsideColor = "bg-yellow-500/30";
insideColor = "bg-yellow-500";
break;
default:
diff --git a/apps/web/src/server/public-api/api/get-domains.ts b/apps/web/src/server/public-api/api/domains/get-domains.ts
similarity index 80%
rename from apps/web/src/server/public-api/api/get-domains.ts
rename to apps/web/src/server/public-api/api/domains/get-domains.ts
index 09db101..629527f 100644
--- a/apps/web/src/server/public-api/api/get-domains.ts
+++ b/apps/web/src/server/public-api/api/domains/get-domains.ts
@@ -1,8 +1,8 @@
import { createRoute, z } from "@hono/zod-openapi";
import { DomainSchema } from "~/lib/zod/domain-schema";
-import { PublicAPIApp } from "../hono";
-import { db } from "../../db";
-import { getTeamFromToken } from "../auth";
+import { PublicAPIApp } from "~/server/public-api/hono";
+import { db } from "~/server/db";
+import { getTeamFromToken } from "~/server/public-api/auth";
const route = createRoute({
method: "get",
diff --git a/apps/web/src/server/public-api/api/send-email.ts b/apps/web/src/server/public-api/api/emails/send-email.ts
similarity index 91%
rename from apps/web/src/server/public-api/api/send-email.ts
rename to apps/web/src/server/public-api/api/emails/send-email.ts
index 0a735ca..ec3cf93 100644
--- a/apps/web/src/server/public-api/api/send-email.ts
+++ b/apps/web/src/server/public-api/api/emails/send-email.ts
@@ -1,6 +1,6 @@
import { createRoute, z } from "@hono/zod-openapi";
-import { PublicAPIApp } from "../hono";
-import { getTeamFromToken } from "../auth";
+import { PublicAPIApp } from "~/server/public-api/hono";
+import { getTeamFromToken } from "~/server/public-api/auth";
import { sendEmail } from "~/server/service/email-service";
const route = createRoute({
diff --git a/apps/web/src/server/public-api/auth.ts b/apps/web/src/server/public-api/auth.ts
index 9f28d8d..5f80d0e 100644
--- a/apps/web/src/server/public-api/auth.ts
+++ b/apps/web/src/server/public-api/auth.ts
@@ -3,6 +3,9 @@ import { hashToken } from "../auth";
import { db } from "../db";
import { UnsendApiError } from "./api-error";
+/**
+ * Gets the team from the token. Also will check if the token is valid.
+ */
export const getTeamFromToken = async (c: Context) => {
const authHeader = c.req.header("Authorization");
if (!authHeader) {
@@ -11,7 +14,7 @@ export const getTeamFromToken = async (c: Context) => {
message: "No Authorization header provided",
});
}
- const token = authHeader.split(" ")[1]; // Assuming the Authorization header is in the format "Bearer
"
+ const token = authHeader.split(" ")[1];
if (!token) {
throw new UnsendApiError({
code: "UNAUTHORIZED",
@@ -38,5 +41,15 @@ export const getTeamFromToken = async (c: Context) => {
});
}
+ // No await so it won't block the request. Need to be moved to a queue in future
+ db.apiKey.update({
+ where: {
+ tokenHash: hashedToken,
+ },
+ data: {
+ lastUsed: new Date(),
+ },
+ });
+
return team;
};
diff --git a/apps/web/src/server/public-api/index.ts b/apps/web/src/server/public-api/index.ts
index 4fc662c..9b41d1d 100644
--- a/apps/web/src/server/public-api/index.ts
+++ b/apps/web/src/server/public-api/index.ts
@@ -1,10 +1,13 @@
import { getApp } from "./hono";
-import getDomains from "./api/get-domains";
-import sendEmail from "./api/send-email";
+import getDomains from "./api/domains/get-domains";
+import sendEmail from "./api/emails/send-email";
export const app = getApp();
+/**Domain related APIs */
getDomains(app);
+
+/**Email related APIs */
sendEmail(app);
export default app;
diff --git a/packages/ui/src/code.tsx b/packages/ui/src/code.tsx
index 3eae0f4..c23512f 100644
--- a/packages/ui/src/code.tsx
+++ b/packages/ui/src/code.tsx
@@ -9,6 +9,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
import { Button } from "./button";
import { ClipboardCopy, Check } from "lucide-react";
import { useState } from "react";
+import { cn } from "../lib/utils";
type Language = "js" | "ruby" | "php" | "python";
@@ -17,6 +18,7 @@ type CodeProps = {
language: Language;
code: string;
}[];
+ codeClassName?: string;
};
SyntaxHighlighter.registerLanguage("js", js);
@@ -24,7 +26,7 @@ SyntaxHighlighter.registerLanguage("ruby", ruby);
SyntaxHighlighter.registerLanguage("php", php);
SyntaxHighlighter.registerLanguage("python", python);
-export const Code: React.FC = ({ codeBlocks }) => {
+export const Code: React.FC = ({ codeBlocks, codeClassName }) => {
const [selectedTab, setSelectedTab] = useState(
codeBlocks[0]?.language ?? "js"
);
@@ -41,7 +43,7 @@ export const Code: React.FC = ({ codeBlocks }) => {
};
return (
-
+
setSelectedTab(val as Language)}
@@ -53,7 +55,7 @@ export const Code: React.FC = ({ codeBlocks }) => {
{block.language}
@@ -81,9 +83,9 @@ export const Code: React.FC = ({ codeBlocks }) => {
-