From 163aca189b433eb4f9577a3152d05b2bda921661 Mon Sep 17 00:00:00 2001 From: KMKoushik Date: Fri, 26 Apr 2024 10:04:59 +1000 Subject: [PATCH] Update landing page --- apps/marketing/src/app/layout.tsx | 2 +- apps/marketing/src/app/page.tsx | 360 +++++++++++++++--- apps/marketing/tailwind.config.ts | 5 +- .../domains/[domainId]/send-test-mail.tsx | 1 + .../app/(dashboard)/emails/email-details.tsx | 2 +- .../(dashboard)/emails/email-status-badge.tsx | 14 +- .../api/{ => domains}/get-domains.ts | 6 +- .../public-api/api/{ => emails}/send-email.ts | 4 +- apps/web/src/server/public-api/auth.ts | 15 +- apps/web/src/server/public-api/index.ts | 7 +- packages/ui/src/code.tsx | 12 +- 11 files changed, 354 insertions(+), 74 deletions(-) rename apps/web/src/server/public-api/api/{ => domains}/get-domains.ts (80%) rename apps/web/src/server/public-api/api/{ => emails}/send-email.ts (91%) 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.
  • -
    +
    -
    -
    +
    +

    Marketing Mail

    • 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 }) => { -
    +
    {block.code}