add languages to landing page
This commit is contained in:
@@ -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 = [
|
||||
|
184
apps/marketing/src/components/CodeExample.tsx
Normal file
184
apps/marketing/src/components/CodeExample.tsx
Normal 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;
|
132
apps/marketing/src/components/CodeLangToggle.tsx
Normal file
132
apps/marketing/src/components/CodeLangToggle.tsx
Normal 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;
|
Reference in New Issue
Block a user