add languages to landing page

This commit is contained in:
KM Koushik
2025-09-09 07:13:15 +10:00
parent 3158ddc51c
commit 292048d2c9
7 changed files with 357 additions and 57 deletions

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.1177 14.0442C17.7408 14.1497 17.3586 14.2566 16.9162 14.3768C16.7001 14.438 16.6509 14.4519 16.4498 14.2074C16.2086 13.9194 16.0317 13.7331 15.6939 13.5636C14.6807 13.0384 13.6996 13.1909 12.7829 13.8178C11.6893 14.5632 11.1264 15.6644 11.1425 17.0367C11.1585 18.3921 12.0431 19.5103 13.3137 19.6966C14.4073 19.8491 15.324 19.4425 16.0477 18.5785C16.1924 18.3922 16.3212 18.1887 16.482 17.9516H13.378C13.0402 17.9516 12.9598 17.7314 13.0724 17.4433C13.2815 16.9181 13.6675 16.0372 13.8926 15.5967C13.9409 15.495 14.0535 15.3256 14.2947 15.3256H19.4702C19.7027 14.5496 20.0799 13.8164 20.5831 13.1226C21.7572 11.4961 23.1725 10.649 25.0863 10.2933C26.7268 9.9883 28.2707 10.1577 29.6699 11.1573C30.9405 12.0722 31.7285 13.3089 31.9376 14.9354C32.211 17.2225 31.5838 19.0862 30.0881 20.6787C29.0266 21.8138 27.7239 22.5254 26.2282 22.8473C25.9429 22.9029 25.6576 22.9293 25.3768 22.9553C25.2303 22.9689 25.085 22.9823 24.9416 22.9998C23.478 22.9659 22.1432 22.5254 21.0173 21.5089C20.2256 20.7879 19.6803 19.9019 19.4092 18.8705C19.2211 19.2707 18.9962 19.6539 18.7336 20.0185C17.5756 21.628 16.0638 22.6276 14.15 22.8987C12.5738 23.1189 11.1103 22.797 9.82366 21.7805C8.63353 20.8317 7.95805 19.578 7.78114 18.0194C7.57206 16.1727 8.08671 14.5124 9.14818 13.0554C10.2901 11.4798 11.8019 10.4802 13.6514 10.1244C15.1632 9.8364 16.6106 10.0228 17.9134 10.9546C18.7657 11.5475 19.3769 12.3608 19.779 13.3434C19.8755 13.4959 19.8111 13.5806 19.6181 13.6314C19.0545 13.7822 18.5903 13.9121 18.1177 14.0442ZM28.7581 15.974C28.7613 16.0309 28.7646 16.0909 28.7693 16.1552C28.6889 17.6122 27.9973 18.6965 26.7268 19.3911C25.8744 19.8485 24.9898 19.8994 24.1053 19.4928C22.9473 18.9506 22.3361 17.6122 22.6256 16.2907C22.9795 14.6982 23.9444 13.6986 25.4401 13.3428C26.968 12.9701 28.4316 13.9188 28.7211 15.5961C28.7438 15.7161 28.7505 15.836 28.7581 15.974Z" fill="#00ACD7"/>
<path d="M2.44461 13.8517C2.41244 13.9025 2.42852 13.9364 2.49285 13.9364L7.2826 13.9534C7.33085 13.9534 7.41126 13.9025 7.44343 13.8517L7.71684 13.4112C7.749 13.3604 7.73292 13.3096 7.66859 13.3096H2.95926C2.89493 13.3096 2.81451 13.3435 2.78235 13.3943L2.44461 13.8517Z" fill="#00ACD7"/>
<path d="M0.0160829 15.4103C-0.0160829 15.4611 7.45058e-09 15.495 0.0643316 15.495L6.63928 15.4781C6.70361 15.4781 6.76794 15.4442 6.78402 15.3764L6.91269 14.9698C6.92877 14.919 6.8966 14.8682 6.83227 14.8682H0.530735C0.466404 14.8682 0.385989 14.902 0.353823 14.9529L0.0160829 15.4103Z" fill="#00ACD7"/>
<path d="M3.90813 16.9521C3.87596 17.0029 3.89204 17.0537 3.95638 17.0537L6.43019 17.0707C6.47843 17.0707 6.54277 17.0199 6.54277 16.9521L6.57493 16.5455C6.57493 16.4777 6.54277 16.4269 6.47843 16.4269H4.29412C4.22978 16.4269 4.16545 16.4777 4.13329 16.5285L3.90813 16.9521Z" fill="#00ACD7"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
aria-label="PHP" role="img"
viewBox="0 0 512 512">
<rect
width="512" height="512"
rx="15%"
fill="#777bb3"/>
<g stroke="#ffffff" stroke-width="6">
<path id="a" d="M155 202H97a3 3 0 0 0-3 2L67 345c0 3 3 3 3 3h30c3 0 3-2 3-2l7-35h27c42 0 63-28 68-53s1-56-50-56zm13 54c-4 19-17 27-35 27h-18l11-53h18c27 0 26 10 24 26z"/>

After

Width:  |  Height:  |  Size: 733 B

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0164 2C10.8193 2 9.03825 3.72453 9.03825 5.85185V8.51852H15.9235V9.25926H5.97814C3.78107 9.25926 2 10.9838 2 13.1111L2 18.8889C2 21.0162 3.78107 22.7407 5.97814 22.7407H8.27322V19.4815C8.27322 17.3542 10.0543 15.6296 12.2514 15.6296H19.5956C21.4547 15.6296 22.9617 14.1704 22.9617 12.3704V5.85185C22.9617 3.72453 21.1807 2 18.9836 2H13.0164ZM12.0984 6.74074C12.8589 6.74074 13.4754 6.14378 13.4754 5.40741C13.4754 4.67103 12.8589 4.07407 12.0984 4.07407C11.3378 4.07407 10.7213 4.67103 10.7213 5.40741C10.7213 6.14378 11.3378 6.74074 12.0984 6.74074Z" fill="url(#paint0_linear_87_8204)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.9834 30C21.1805 30 22.9616 28.2755 22.9616 26.1482V23.4815L16.0763 23.4815L16.0763 22.7408L26.0217 22.7408C28.2188 22.7408 29.9998 21.0162 29.9998 18.8889V13.1111C29.9998 10.9838 28.2188 9.25928 26.0217 9.25928L23.7266 9.25928V12.5185C23.7266 14.6459 21.9455 16.3704 19.7485 16.3704L12.4042 16.3704C10.5451 16.3704 9.03809 17.8296 9.03809 19.6296L9.03809 26.1482C9.03809 28.2755 10.8192 30 13.0162 30H18.9834ZM19.9015 25.2593C19.1409 25.2593 18.5244 25.8562 18.5244 26.5926C18.5244 27.329 19.1409 27.9259 19.9015 27.9259C20.662 27.9259 21.2785 27.329 21.2785 26.5926C21.2785 25.8562 20.662 25.2593 19.9015 25.2593Z" fill="url(#paint1_linear_87_8204)"/>
<defs>
<linearGradient id="paint0_linear_87_8204" x1="12.4809" y1="2" x2="12.4809" y2="22.7407" gradientUnits="userSpaceOnUse">
<stop stop-color="#327EBD"/>
<stop offset="1" stop-color="#1565A7"/>
</linearGradient>
<linearGradient id="paint1_linear_87_8204" x1="19.519" y1="9.25928" x2="19.519" y2="30" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFDA4B"/>
<stop offset="1" stop-color="#F9C600"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><title>file_type_typescript_official</title><rect x="2" y="2" width="28" height="28" rx="1.312" style="fill:#3178c6"/><path d="M18.245,23.759v3.068a6.492,6.492,0,0,0,1.764.575,11.56,11.56,0,0,0,2.146.192,9.968,9.968,0,0,0,2.088-.211,5.11,5.11,0,0,0,1.735-.7,3.542,3.542,0,0,0,1.181-1.266,4.469,4.469,0,0,0,.186-3.394,3.409,3.409,0,0,0-.717-1.117,5.236,5.236,0,0,0-1.123-.877,12.027,12.027,0,0,0-1.477-.734q-.6-.249-1.08-.484a5.5,5.5,0,0,1-.813-.479,2.089,2.089,0,0,1-.516-.518,1.091,1.091,0,0,1-.181-.618,1.039,1.039,0,0,1,.162-.571,1.4,1.4,0,0,1,.459-.436,2.439,2.439,0,0,1,.726-.283,4.211,4.211,0,0,1,.956-.1,5.942,5.942,0,0,1,.808.058,6.292,6.292,0,0,1,.856.177,5.994,5.994,0,0,1,.836.3,4.657,4.657,0,0,1,.751.422V13.9a7.509,7.509,0,0,0-1.525-.4,12.426,12.426,0,0,0-1.9-.129,8.767,8.767,0,0,0-2.064.235,5.239,5.239,0,0,0-1.716.733,3.655,3.655,0,0,0-1.171,1.271,3.731,3.731,0,0,0-.431,1.845,3.588,3.588,0,0,0,.789,2.34,6,6,0,0,0,2.395,1.639q.63.26,1.175.509a6.458,6.458,0,0,1,.942.517,2.463,2.463,0,0,1,.626.585,1.2,1.2,0,0,1,.23.719,1.1,1.1,0,0,1-.144.552,1.269,1.269,0,0,1-.435.441,2.381,2.381,0,0,1-.726.292,4.377,4.377,0,0,1-1.018.105,5.773,5.773,0,0,1-1.969-.35A5.874,5.874,0,0,1,18.245,23.759Zm-5.154-7.638h4V13.594H5.938v2.527H9.92V27.375h3.171Z" style="fill:#ffffff;fill-rule:evenodd"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -7,7 +7,7 @@ import { TopNav } from "~/components/TopNav";
import { FeatureCard } from "~/components/FeatureCard";
import { FeatureCardPlain } from "~/components/FeatureCardPlain";
import { PricingCalculator } from "~/components/PricingCalculator";
import { CodeBlock } from "@usesend/ui/src/code-block";
import CodeExample from "~/components/CodeExample";
const REPO = "usesend/usesend";
const REPO_URL = `https://github.com/${REPO}`;
@@ -292,62 +292,7 @@ function Features() {
);
}
function CodeExample() {
const code = `import { UseSend } from "usesend-js";
const usesend = new UseSend("us_12345");
usesend.emails.send({
to: "hello@acme.com",
from: "hello@company.com",
subject: "useSend email",
html: "<p>useSend is the best open source product to send emails</p>",
text: "useSend is the best open source product to send emails",
});`;
return (
<section className="py-16 sm:py-20">
<div className="mx-auto max-w-6xl px-6">
<div className="text-center">
<div className="mb-2 text-sm uppercase tracking-wider text-primary">
Developers
</div>
<p className="mt-1 text-xs sm:text-sm text-muted-foreground max-w-2xl mx-auto">
Typed SDKs and simple APIs, so you can focus on product not
plumbing.
</p>
</div>
<div className="mt-8 overflow-hidden">
<div className=" py-2 text-xs text-muted-foreground">JavaScript</div>
<div className="rounded-[18px] bg-primary/20 p-1">
<div className="rounded-[14px] bg-primary/20 p-0.5 shadow-sm">
<div className="bg-background rounded-xl overflow-hidden">
<CodeBlock
lang="javascript"
children={code}
className="p-4 rounded-[10px]"
/>
</div>
</div>
</div>
</div>
<div className="mt-6 flex items-center justify-center gap-3">
<Button size="lg" className="px-6">
<a
href="https://docs.usesend.com"
target="_blank"
rel="noopener noreferrer"
>
Read the docs
</a>
</Button>
</div>
</div>
</section>
);
}
// CodeExample moved to a dedicated client component in ~/components/CodeExample
function Pricing() {
const freePerks = [

View File

@@ -0,0 +1,184 @@
import { Button } from "@usesend/ui/src/button";
import { CodeBlock } from "@usesend/ui/src/code-block";
import { LangToggle } from "./CodeLangToggle";
const TS_CODE = `import { UseSend } from "usesend-js";
const usesend = new UseSend("us_12345");
usesend.emails.send({
to: "hello@acme.com",
from: "hello@company.com",
subject: "useSend email",
html: "<p>useSend is the best open source product to send emails</p>",
text: "useSend is the best open source product to send emails",
});`;
const PY_CODE = `from usesend import UseSend
client = UseSend("us_12345")
data, err = client.emails.send({
"to": "hello@acme.com",
"from": "hello@company.com",
"subject": "useSend email",
"html": "<p>useSend is the best open source product to send emails</p>",
"text": "useSend is the best open source product to send emails",
})
print(data or err)`;
const GO_CODE = `package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
url := "https://app.usesend.com/api/v1/emails"
payload := strings.NewReader("{\n \\\"to\\\": \\\"hello@acme.com\\\",\n \\\"from\\\": \\\"hello@company.com\\\",\n \\\"subject\\\": \\\"useSend email\\\",\n \\\"html\\\": \\\"<p>useSend is the best open source product to send emails</p>\\\",\n \\\"text\\\": \\\"useSend is the best open source product to send emails\\\"\n }")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer us_12345")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(res)
fmt.Println(string(body))
}`;
const PHP_CODE = `<?php
$ch = curl_init('https://app.usesend.com/api/v1/emails');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer us_12345',
],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode([
'to' => 'hello@acme.com',
'from' => 'hello@company.com',
'subject' => 'useSend email',
'html' => '<p>useSend is the best open source product to send emails</p>',
'text' => 'useSend is the best open source product to send emails',
]),
]);
$response = curl_exec($ch);
if ($response === false) {
echo 'cURL error: ' . curl_error($ch);
} else {
echo $response;
}
curl_close($ch);`;
export function CodeExample() {
const containerId = "code-example";
const languages = [
{
key: "ts",
label: "TypeScript",
kind: "ts",
shiki: "typescript" as const,
code: TS_CODE,
},
{
key: "py",
label: "Python",
kind: "py",
shiki: "python" as const,
code: PY_CODE,
},
{
key: "go",
label: "Go",
kind: "go",
shiki: "go" as const,
code: GO_CODE,
},
{
key: "php",
label: "PHP",
kind: "php",
shiki: "php" as const,
code: PHP_CODE,
},
];
return (
<section className="py-16 sm:py-20">
<div className="mx-auto max-w-6xl px-6">
<div className="text-center">
<div className="mb-2 text-sm uppercase tracking-wider text-primary">
Developers
</div>
<p className="mt-1 text-xs sm:text-sm text-muted-foreground max-w-2xl mx-auto">
Typed SDKs and simple APIs, so you can focus on product not
plumbing.
</p>
</div>
<div className="mt-8 overflow-hidden" id={containerId}>
<div className="flex items-center gap-2 justify-center py-2 text-xs text-muted-foreground mb-4">
<LangToggle
containerId={containerId}
defaultLang="ts"
languages={languages.map(({ key, label, kind }) => ({
key,
label,
kind,
}))}
/>
</div>
<div className="rounded-[18px] bg-primary/20 p-1">
<div className="rounded-[14px] bg-primary/20 p-0.5 shadow-sm">
<div className="bg-background rounded-xl overflow-hidden">
{languages.map((l, idx) => (
<div
key={l.key}
data-lang-slot={l.key}
className={idx === 0 ? "block" : "hidden"}
>
{/* Cast to any to align with shiki BundledLanguage without importing types here */}
<CodeBlock
lang={l.shiki as any}
className="p-4 rounded-[10px]"
>
{l.code}
</CodeBlock>
</div>
))}
</div>
</div>
</div>
<div className="sr-only" aria-live="polite">
Language example toggled
</div>
</div>
<div className="mt-6 flex items-center justify-center gap-3">
<Button size="lg" className="px-6">
<a
href="https://docs.usesend.com"
target="_blank"
rel="noopener noreferrer"
>
Read the docs
</a>
</Button>
</div>
</div>
</section>
);
}
export default CodeExample;

View File

@@ -0,0 +1,132 @@
"use client";
import { useEffect, useState } from "react";
import Image from "next/image";
import { Button } from "@usesend/ui/src/button";
type LangItem = {
key: string;
label: string;
kind: "ts" | "py" | string; // used for icon selection
};
export function LangToggle({
containerId,
languages,
defaultLang,
}: {
containerId: string;
languages: LangItem[];
defaultLang: string;
}) {
const [active, setActive] = useState(defaultLang);
useEffect(() => {
const container = document.getElementById(containerId);
if (!container) return;
const slots = Array.from(
container.querySelectorAll<HTMLElement>("[data-lang-slot]")
);
for (const el of slots) {
const key = el.getAttribute("data-lang-slot");
if (key === active) {
el.classList.remove("hidden");
el.classList.add("block");
} else {
el.classList.add("hidden");
el.classList.remove("block");
}
}
}, [active, containerId]);
return (
<div className="flex items-center gap-2 justify-center">
{languages.map((l) => (
<Button
key={l.key}
size="sm"
variant="outline"
className={
"px-3 bg-transparent hover:bg-transparent hover:text-inherit " +
(active === l.key
? "border-primary"
: "border-input")
}
aria-pressed={active === l.key}
onClick={() => setActive(l.key)}
>
<span className="inline-flex items-center">
<LangIcon kind={l.kind} className="h-4 w-4 mr-1" /> {l.label}
</span>
</Button>
))}
</div>
);
}
function LangIcon({ kind, className = "h-4 w-4" }: { kind: string; className?: string }) {
const [failed, setFailed] = useState(false);
if (failed) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" className={className} role="img">
<circle cx="12" cy="12" r="10" fill="currentColor" opacity="0.2" />
</svg>
);
}
if (kind === "ts")
return (
<Image
src="/typescript.svg"
alt="TypeScript logo"
width={16}
height={16}
className={className}
priority={false}
onError={() => setFailed(true)}
/>
);
if (kind === "py")
return (
<Image
src="/python.svg"
alt="Python logo"
width={16}
height={16}
className={className}
priority={false}
onError={() => setFailed(true)}
/>
);
if (kind === "go")
return (
<Image
src="/go.svg"
alt="Go logo"
width={16}
height={16}
className={className}
priority={false}
onError={() => setFailed(true)}
/>
);
if (kind === "php")
return (
<Image
src="/php.svg"
alt="PHP logo"
width={16}
height={16}
className={className}
priority={false}
onError={() => setFailed(true)}
/>
);
return (
<svg viewBox="0 0 24 24" aria-hidden="true" className={className} role="img">
<circle cx="12" cy="12" r="10" fill="currentColor" opacity="0.2" />
</svg>
);
}
export default LangToggle;