initial commit. gotta go

This commit is contained in:
2025-09-26 14:30:57 -05:00
parent b342335502
commit eb0b35bb7f
299 changed files with 6902 additions and 6741 deletions

View File

@@ -1,55 +1,55 @@
import "@usesend/ui/styles/globals.css";
import '@usesend/ui/styles/globals.css';
import { Inter } from "next/font/google";
import { JetBrains_Mono } from "next/font/google";
import type { Metadata } from "next";
import { ThemeProvider } from "@usesend/ui";
import Script from "next/script";
import { Inter } from 'next/font/google';
import { JetBrains_Mono } from 'next/font/google';
import type { Metadata } from 'next';
import { ThemeProvider } from '@usesend/ui';
import Script from 'next/script';
const inter = Inter({
subsets: ["latin"],
variable: "--font-sans",
subsets: ['latin'],
variable: '--font-sans',
});
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-mono",
subsets: ['latin'],
variable: '--font-mono',
});
export const metadata: Metadata = {
title: "useSend Open source email platform",
description: "Pay only for what you send, not for storing contacts",
icons: [{ rel: "icon", url: "/favicon.ico" }],
metadataBase: new URL("https://usesend.com"),
title: 'useSend Open source email platform',
description: 'Pay only for what you send, not for storing contacts',
icons: [{ rel: 'icon', url: '/favicon.ico' }],
metadataBase: new URL('https://usesend.com'),
openGraph: {
title: "useSend Open source email platform",
description: "Pay only for what you send, not for storing contacts",
url: "https://usesend.com",
siteName: "useSend",
title: 'useSend Open source email platform',
description: 'Pay only for what you send, not for storing contacts',
url: 'https://usesend.com',
siteName: 'useSend',
images: [
{
url: "https://uploads.usesend.com/logos/og.png",
url: 'https://uploads.usesend.com/logos/og.png',
width: 1200,
height: 630,
alt: "useSend Open source email platform",
type: "image/png",
alt: 'useSend Open source email platform',
type: 'image/png',
},
],
locale: "en_US",
type: "website",
locale: 'en_US',
type: 'website',
},
twitter: {
card: "summary_large_image",
title: "useSend Open source email platform",
description: "Pay only for what you send, not for storing contacts",
images: ["https://uploads.usesend.com/logos/og.png"],
card: 'summary_large_image',
title: 'useSend Open source email platform',
description: 'Pay only for what you send, not for storing contacts',
images: ['https://uploads.usesend.com/logos/og.png'],
},
robots: {
index: true,
follow: true,
},
alternates: {
canonical: "https://usesend.com",
canonical: 'https://usesend.com',
},
};
@@ -62,9 +62,9 @@ export default function RootLayout({
<html
lang="en"
suppressHydrationWarning
className="scroll-smooth bg-background"
className="bg-background scroll-smooth"
>
{process.env.NODE_ENV === "production" && (
{process.env.NODE_ENV === 'production' && (
<Script src="https://scripts.simpleanalyticscdn.com/latest.js" />
)}
<body

View File

@@ -1,21 +1,21 @@
import Image from "next/image";
import Link from "next/link";
import { SiteFooter } from "~/components/SiteFooter";
import { GitHubStarsButton } from "~/components/GitHubStarsButton";
import { Button } from "@usesend/ui/src/button";
import { TopNav } from "~/components/TopNav";
import { FeatureCard } from "~/components/FeatureCard";
import { FeatureCardPlain } from "~/components/FeatureCardPlain";
import { PricingCalculator } from "~/components/PricingCalculator";
import CodeExample from "~/components/CodeExample";
import Image from 'next/image';
import Link from 'next/link';
import { SiteFooter } from '~/components/SiteFooter';
import { GitHubStarsButton } from '~/components/GitHubStarsButton';
import { Button } from '@usesend/ui/src/button';
import { TopNav } from '~/components/TopNav';
import { FeatureCard } from '~/components/FeatureCard';
import { FeatureCardPlain } from '~/components/FeatureCardPlain';
import { PricingCalculator } from '~/components/PricingCalculator';
import CodeExample from '~/components/CodeExample';
const REPO = "usesend/usesend";
const REPO = 'usesend/usesend';
const REPO_URL = `https://github.com/${REPO}`;
const APP_URL = "https://app.usesend.com";
const APP_URL = 'https://app.usesend.com';
export default function Page() {
return (
<main className="min-h-screen text-foreground bg-background">
<main className="text-foreground bg-background min-h-screen">
<TopNav />
<Hero />
<TrustedBy />
@@ -34,18 +34,18 @@ function Hero() {
return (
<section>
<div className="mx-auto max-w-6xl px-6 py-16 sm:py-24">
<h1 className="mt-6 text-center text-2xl sm:text-4xl font-semibold text-primary font-sans">
<h1 className="text-primary mt-6 text-center font-sans text-2xl font-semibold sm:text-4xl">
The open source email platform for everyone
</h1>
<p className="mt-4 text-center text-base sm:text-lg font-sans max-w-2xl mx-auto">
Send product, transactional and marketing emails.{" "}
<p className="mx-auto mt-4 max-w-2xl text-center font-sans text-base sm:text-lg">
Send product, transactional and marketing emails.{' '}
<span className="text-primary font-normal">
Pay only for what you send
</span>{" "}
</span>{' '}
and not for storing contacts.
</p>
<div className="mt-8 flex flex-col sm:flex-row items-center justify-center gap-3">
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Button size="lg" className="px-6">
<a href={APP_URL} target="_blank" rel="noopener noreferrer">
Get started
@@ -55,11 +55,11 @@ function Hero() {
<GitHubStarsButton />
</div>
<p className="mt-3 text-center text-xs text-muted-foreground">
<p className="text-muted-foreground mt-3 text-center text-xs">
Open source Self-host in minutes Free tier
</p>
<div className="mt-12 text-center text-xs text-muted-foreground flex flex-col items-center justify-center gap-2">
<div className="text-muted-foreground mt-12 flex flex-col items-center justify-center gap-2 text-center text-xs">
<p className="text-xs">Proudly sponsored by</p>
<a
href="https://coderabbit.ai/?utm_source=useSend.com"
@@ -89,15 +89,15 @@ function Hero() {
</a>
</div>
<div className=" mt-32 mx-auto max-w-5xl">
<div className="rounded-[18px] bg-primary/10 p-1 sm:p-1 ">
<div className="rounded-2xl bg-primary/20 p-1 sm:p-1 ">
<div className="mx-auto mt-32 max-w-5xl">
<div className="bg-primary/10 rounded-[18px] p-1 sm:p-1">
<div className="bg-primary/20 rounded-2xl p-1 sm:p-1">
<Image
src="/hero-light.webp"
alt="useSend product hero"
width={3456}
height={1914}
className="w-full h-auto rounded-xl block dark:hidden"
className="block h-auto w-full rounded-xl dark:hidden"
sizes="(min-width: 1024px) 900px, 100vw"
loading="eager"
priority={false}
@@ -107,7 +107,7 @@ function Hero() {
alt="useSend product hero"
width={3456}
height={1914}
className="w-full h-auto rounded-xl hidden dark:block"
className="hidden h-auto w-full rounded-xl dark:block"
sizes="(min-width: 1024px) 900px, 100vw"
loading="eager"
priority={false}
@@ -127,61 +127,61 @@ function TrustedBy() {
{
quote:
"Transitioned recently to open source email sender useSend for our 30k and growing newsletter. It's such a great product and amazing oss experience.",
author: "Marc Seitz",
company: "papermark.com",
author: 'Marc Seitz',
company: 'papermark.com',
image:
"https://pbs.twimg.com/profile_images/1176854646343852032/iYnUXJ-m_400x400.jpg",
'https://pbs.twimg.com/profile_images/1176854646343852032/iYnUXJ-m_400x400.jpg',
},
{
quote:
"useSend was extremely easy to set up, and I love that it's open source. Koushik has been an absolute awesome person to deal with and helps us with any issues or feedback.",
author: "Tommerty",
company: "doras.to",
author: 'Tommerty',
company: 'doras.to',
image:
"https://cdn.doras.to/doras/user/83bda65b-8d42-4011-9bf0-ab23402776f2-0.890688178917765.webp",
'https://cdn.doras.to/doras/user/83bda65b-8d42-4011-9bf0-ab23402776f2-0.890688178917765.webp',
},
];
const quick = [
{
quote: "don't sleep on useSend",
author: "shellscape",
company: "jsx.email",
author: 'shellscape',
company: 'jsx.email',
image:
"https://pbs.twimg.com/profile_images/1698447401781022720/b0DZSc_D_400x400.jpg",
'https://pbs.twimg.com/profile_images/1698447401781022720/b0DZSc_D_400x400.jpg',
},
{
quote: "Thank you for making useSend!",
author: "Andras Bacsai",
company: "coolify.io",
quote: 'Thank you for making useSend!',
author: 'Andras Bacsai',
company: 'coolify.io',
image:
"https://pbs.twimg.com/profile_images/1884210412524027905/jW4NB4rx_400x400.jpg",
'https://pbs.twimg.com/profile_images/1884210412524027905/jW4NB4rx_400x400.jpg',
},
{
quote: "I KNOW WHAT TO DO",
author: "VicVijayakumar",
company: "onetimefax.com",
quote: 'I KNOW WHAT TO DO',
author: 'VicVijayakumar',
company: 'onetimefax.com',
image:
"https://pbs.twimg.com/profile_images/1665351804685524995/W4BpDx5Z_400x400.jpg",
'https://pbs.twimg.com/profile_images/1665351804685524995/W4BpDx5Z_400x400.jpg',
},
];
return (
<section className="py-10 sm:py-20 ">
<section className="py-10 sm:py-20">
<div className="mx-auto max-w-6xl px-6">
<div className="text-center tracking-wider text-muted-foreground">
<div className="text-muted-foreground text-center tracking-wider">
<span className="">Builders and open source teams love </span>
<span className="text-primary font-bold">useSend</span>
</div>
{/* Top: 2 larger testimonials */}
<div className="mt-6 grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2">
{featured.map((t) => (
<figure
key={t.author + t.company}
className="rounded-xl border border-primary/30 p-5 h-full"
className="border-primary/30 h-full rounded-xl border p-5"
>
<blockquote className="text-sm sm:text-base font-light font-sans ">
<blockquote className="font-sans text-sm font-light sm:text-base">
{t.quote}
</blockquote>
<div className="mt-5 flex items-center gap-3">
@@ -190,7 +190,7 @@ function TrustedBy() {
alt={`${t.author} avatar`}
width={32}
height={32}
className=" rounded-md border-2 border-primary/50"
className="border-primary/50 rounded-md border-2"
/>
<figcaption className="text-sm">
<span className="font-medium">{t.author}</span>
@@ -199,9 +199,9 @@ function TrustedBy() {
target="_blank"
className="text-muted-foreground hover:text-primary-light"
>
{" "}
{' '}
{t.company}
</a>{" "}
</a>{' '}
</figcaption>
</div>
</figure>
@@ -209,13 +209,13 @@ function TrustedBy() {
</div>
{/* Bottom: 3 multi-line testimonials (same style as top) */}
<div className="mt-4 grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-3">
{quick.map((t) => (
<figure
key={t.author + t.company}
className="rounded-xl border border-primary/30 p-5 h-full"
className="border-primary/30 h-full rounded-xl border p-5"
>
<blockquote className="text-sm sm:text-base font-light font-sans leading-relaxed">
<blockquote className="font-sans text-sm font-light leading-relaxed sm:text-base">
{t.quote}
</blockquote>
<div className="mt-5 flex items-center gap-3">
@@ -224,7 +224,7 @@ function TrustedBy() {
alt={`${t.author} avatar`}
width={32}
height={32}
className=" rounded-md border-2 border-primary/50"
className="border-primary/50 rounded-md border-2"
/>
<figcaption className="text-sm">
<span className="font-medium">{t.author}</span>
@@ -233,7 +233,7 @@ function TrustedBy() {
target="_blank"
className="text-muted-foreground hover:text-primary-light"
>
{" "}
{' '}
{t.company}
</a>
</figcaption>
@@ -250,42 +250,42 @@ function Features() {
// Top: 2 cards (with image area) — Analytics, Editor
const top = [
{
key: "feature-analytics",
title: "Analytics",
key: 'feature-analytics',
title: 'Analytics',
content:
"Track deliveries, opens, clicks, bounces and unsubscribes in real time with a simple, searchable log. Filter by domain, status, api key and export them. Track which campaigns perform best.",
imageLightSrc: "/emails-search-light.webp",
imageDarkSrc: "/emails-search-dark.webp",
'Track deliveries, opens, clicks, bounces and unsubscribes in real time with a simple, searchable log. Filter by domain, status, api key and export them. Track which campaigns perform best.',
imageLightSrc: '/emails-search-light.webp',
imageDarkSrc: '/emails-search-dark.webp',
},
{
key: "feature-editor",
title: "Marketing Email Editor",
key: 'feature-editor',
title: 'Marketing Email Editor',
content:
"Design beautiful campaigns without code using a visual, notion like WYSIWYG editor that works in major email clients. Reuse templates and brand styles, and personalize with variables.",
imageLightSrc: "/editor-light.webp",
imageDarkSrc: "/editor-dark.webp",
'Design beautiful campaigns without code using a visual, notion like WYSIWYG editor that works in major email clients. Reuse templates and brand styles, and personalize with variables.',
imageLightSrc: '/editor-light.webp',
imageDarkSrc: '/editor-dark.webp',
},
];
// Bottom: 3 cards (no images) — Contact Management, Suppression List, SMTP Relay Service
const bottom = [
{
key: "feature-contacts",
title: "Contact Management",
key: 'feature-contacts',
title: 'Contact Management',
content:
"Manage contacts, lists, and consent in one place. Import and export easily, keep per-list subscription status. Contacts are automatically updated from bounces and complaints.",
'Manage contacts, lists, and consent in one place. Import and export easily, keep per-list subscription status. Contacts are automatically updated from bounces and complaints.',
},
{
key: "feature-suppression",
title: "Suppression List",
key: 'feature-suppression',
title: 'Suppression List',
content:
"Prevent accidental sends. Automatically populated from bounces and complaints, and manage via import/export or API. Works with transactional and marketing emails.",
'Prevent accidental sends. Automatically populated from bounces and complaints, and manage via import/export or API. Works with transactional and marketing emails.',
},
{
key: "feature-smtp",
title: "SMTP Relay",
key: 'feature-smtp',
title: 'SMTP Relay',
content:
"Drop-in SMTP relay that works with any app or framework. Do not get vendor lock-in. Comes in handy with services like Supabase",
'Drop-in SMTP relay that works with any app or framework. Do not get vendor lock-in. Comes in handy with services like Supabase',
},
];
@@ -293,13 +293,13 @@ function Features() {
<section id="features" 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">
<div className="text-primary mb-2 text-sm uppercase tracking-wider">
Features
</div>
</div>
{/* Top row: 2 side-by-side cards with images */}
<div className="mt-8 grid grid-cols-1 sm:grid-cols-2 gap-6">
<div className="mt-8 grid grid-cols-1 gap-6 sm:grid-cols-2">
{top.map((f) => (
<FeatureCard
key={f.key}
@@ -312,7 +312,7 @@ function Features() {
</div>
{/* Bottom row: 3 cards without images */}
<div className="mt-6 grid grid-cols-1 sm:grid-cols-3 gap-6">
<div className="mt-6 grid grid-cols-1 gap-6 sm:grid-cols-3">
{bottom.map((f) => (
<FeatureCardPlain key={f.key} title={f.title} content={f.content} />
))}
@@ -326,35 +326,35 @@ function Features() {
function Pricing() {
const freePerks = [
"Send up to 3000 emails per month",
"Send up to 100 emails per day",
"Can have 1 contact book",
"Can have 1 domain",
"Can have 1 team member",
'Send up to 3000 emails per month',
'Send up to 100 emails per day',
'Can have 1 contact book',
'Can have 1 domain',
'Can have 1 team member',
];
const paidPerks = [
"$10 monthly usage credits",
"Send transactional emails at $0.0004 per email",
"Send marketing emails at $0.001 per email",
"Can have unlimited contact books",
"Can have unlimited domains",
"Can have unlimited team members",
'$10 monthly usage credits',
'Send transactional emails at $0.0004 per email',
'Send marketing emails at $0.001 per email',
'Can have unlimited contact books',
'Can have unlimited domains',
'Can have unlimited team members',
];
return (
<section id="pricing" 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">
<div className="text-primary mb-2 text-sm uppercase tracking-wider">
PRICING
</div>
<p className="mt-1 text-xs sm:text-sm text-muted-foreground max-w-2xl mx-auto">
<p className="text-muted-foreground mx-auto mt-1 max-w-2xl text-xs sm:text-sm">
pay for what you use, the most affordable email platform
</p>
</div>
<div className="mt-8 grid grid-cols-1 sm:grid-cols-2 gap-6">
<div className="mt-8 grid grid-cols-1 gap-6 sm:grid-cols-2">
<PricingCard
title="Free"
price="$0"
@@ -386,16 +386,16 @@ type PricingCardProps = {
function PricingCard({ title, price, note, perks }: PricingCardProps) {
return (
<div className="rounded-[18px] bg-primary/20 p-1">
<div className="h-full rounded-[14px] bg-primary/20 p-0.5 shadow-sm">
<div className="bg-background rounded-xl h-full flex flex-col p-5">
<h3 className=" font-medium">{title}</h3>
<div className="mt-2 text-4xl text-primary">{price}</div>
<div className="text-xs text-muted-foreground">{note}</div>
<ul className="mt-4 space-y-2 text-sm mb-20">
<div className="bg-primary/20 rounded-[18px] p-1">
<div className="bg-primary/20 h-full rounded-[14px] p-0.5 shadow-sm">
<div className="bg-background flex h-full flex-col rounded-xl p-5">
<h3 className="font-medium">{title}</h3>
<div className="text-primary mt-2 text-4xl">{price}</div>
<div className="text-muted-foreground text-xs">{note}</div>
<ul className="mb-20 mt-4 space-y-2 text-sm">
{perks.map((perk) => (
<li key={perk} className="flex items-start gap-2">
<CheckIcon className="w-4 h-4 mt-0.5 text-primary" />
<CheckIcon className="text-primary mt-0.5 h-4 w-4" />
<span>{perk}</span>
</li>
))}
@@ -422,12 +422,12 @@ function About() {
<section id="about" 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">
<div className="text-primary mb-2 text-sm uppercase tracking-wider">
About
</div>
</div>
<div className="mt-8 max-w-3xl mx-auto text-sm sm:text-base space-y-4">
<div className="mx-auto mt-8 max-w-3xl space-y-4 text-sm sm:text-base">
<p>
As most of email products out there, useSend also uses Amazon SES
under the hood to send emails. We provide an open and alternative
@@ -435,7 +435,7 @@ function About() {
</p>
<p>
useSend is bootstrapped and funded by the cloud offering and
sponsors. If you self host useSend, please consider{" "}
sponsors. If you self host useSend, please consider{' '}
<a
href="https://github.com/sponsors/KMKoushik"
target="_blank"
@@ -456,7 +456,7 @@ function About() {
// Footer moved to ~/components/SiteFooter
// Minimal inline icons (stroke-based, sleek)
function CheckIcon({ className = "" }: { className?: string }) {
function CheckIcon({ className = '' }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,17 +1,17 @@
import type { Metadata } from "next";
import { TopNav } from "~/components/TopNav";
import type { Metadata } from 'next';
import { TopNav } from '~/components/TopNav';
export const metadata: Metadata = {
title: "Privacy Policy useSend",
description: "Simple privacy policy for the useSend marketing site.",
title: 'Privacy Policy useSend',
description: 'Simple privacy policy for the useSend marketing site.',
};
export default function PrivacyPage() {
return (
<main className="min-h-screen bg-sidebar-background text-foreground">
<main className="bg-sidebar-background text-foreground min-h-screen">
<TopNav />
<div className="mx-auto max-w-3xl px-6 py-16">
<h1 className="text-3xl font-semibold tracking-tight mb-6">
<h1 className="mb-6 text-3xl font-semibold tracking-tight">
Privacy Policy
</h1>
<p className="text-muted-foreground mb-8">
@@ -22,7 +22,7 @@ export default function PrivacyPage() {
occasional marketing emails.
</p>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">Who We Are</h2>
<p className="text-muted-foreground">
useSend ("we", "us") operates the marketing website at
@@ -41,13 +41,13 @@ export default function PrivacyPage() {
</p>
</section>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">What We Collect</h2>
<ul className="list-disc pl-5 space-y-2 text-muted-foreground">
<ul className="text-muted-foreground list-disc space-y-2 pl-5">
<li>
<span className="text-foreground">
Usage and device data (marketing site):
</span>{" "}
</span>{' '}
We use Simple Analytics to understand overall traffic and usage
patterns (e.g., pages visited, referrers, device type). Simple
Analytics is a privacyfriendly analytics provider and does not
@@ -55,7 +55,7 @@ export default function PrivacyPage() {
identify you.
</li>
<li>
<span className="text-foreground">Server and security logs:</span>{" "}
<span className="text-foreground">Server and security logs:</span>{' '}
Our hosting providers (Vercel for the marketing site; Railway for
the app) may process IP addresses and basic request metadata
transiently for security, reliability, and debugging.
@@ -63,7 +63,7 @@ export default function PrivacyPage() {
<li>
<span className="text-foreground">
Account and email data (product):
</span>{" "}
</span>{' '}
If you sign up for useSend, we process your account information
and send transactional emails. If you opt in, we may also send
occasional marketing emails. You can unsubscribe at any time via
@@ -72,9 +72,9 @@ export default function PrivacyPage() {
</ul>
</section>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">How We Use Information</h2>
<ul className="list-disc pl-5 space-y-2 text-muted-foreground">
<ul className="text-muted-foreground list-disc space-y-2 pl-5">
<li>Operate, secure, and maintain the marketing site and app.</li>
<li>
Understand aggregated usage to improve performance and content.
@@ -87,7 +87,7 @@ export default function PrivacyPage() {
</ul>
</section>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">Legal Bases</h2>
<p className="text-muted-foreground">
Where applicable (e.g., in the EEA/UK), we rely on legitimate
@@ -97,13 +97,13 @@ export default function PrivacyPage() {
</p>
</section>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">Sharing and Processors</h2>
<p className="text-muted-foreground">
We share information with service providers who process data on our
behalf, including:
</p>
<ul className="list-disc pl-5 space-y-2 text-muted-foreground">
<ul className="text-muted-foreground list-disc space-y-2 pl-5">
<li>
<span className="text-foreground">Hosting:</span> Vercel
(marketing site) and Railway (application) for serving content,
@@ -127,7 +127,7 @@ export default function PrivacyPage() {
</p>
</section>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">Retention</h2>
<p className="text-muted-foreground">
We retain information only for as long as necessary to fulfill the
@@ -137,7 +137,7 @@ export default function PrivacyPage() {
</p>
</section>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">International Transfers</h2>
<p className="text-muted-foreground">
Our providers may process data in locations outside of your country
@@ -146,7 +146,7 @@ export default function PrivacyPage() {
</p>
</section>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">Your Rights</h2>
<p className="text-muted-foreground">
Depending on your location, you may have rights to access, correct,
@@ -158,7 +158,7 @@ export default function PrivacyPage() {
</p>
</section>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">Contact</h2>
<p className="text-muted-foreground">
For privacy requests or questions, email us at
@@ -172,7 +172,7 @@ export default function PrivacyPage() {
</p>
</section>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">Children</h2>
<p className="text-muted-foreground">
Our services are not directed to children, and we do not knowingly
@@ -180,7 +180,7 @@ export default function PrivacyPage() {
</p>
</section>
<section className="space-y-3 mb-10">
<section className="mb-10 space-y-3">
<h2 className="text-xl font-medium">Changes</h2>
<p className="text-muted-foreground">
We may update this policy from time to time. The "Last updated" date
@@ -188,7 +188,7 @@ export default function PrivacyPage() {
</p>
</section>
<p className="text-xs text-muted-foreground">
<p className="text-muted-foreground text-xs">
Last updated: {new Date().toLocaleDateString()}
</p>
</div>

View File

@@ -1,17 +1,19 @@
import type { Metadata } from "next";
import { TopNav } from "~/components/TopNav";
import type { Metadata } from 'next';
import { TopNav } from '~/components/TopNav';
export const metadata: Metadata = {
title: "Terms of Service useSend",
description: "Terms governing use of the useSend website and product.",
title: 'Terms of Service useSend',
description: 'Terms governing use of the useSend website and product.',
};
export default function TermsPage() {
return (
<main className="min-h-screen bg-sidebar-background text-foreground">
<main className="bg-sidebar-background text-foreground min-h-screen">
<TopNav />
<div className="mx-auto max-w-3xl px-6 py-16">
<h1 className="text-3xl font-semibold tracking-tight mb-6">Terms of Service</h1>
<h1 className="mb-6 text-3xl font-semibold tracking-tight">
Terms of Service
</h1>
<p className="text-muted-foreground mb-6">
These Terms of Service ("Terms") govern your access to and use of the
useSend marketing website at usesend.com and the useSend application.
@@ -19,7 +21,7 @@ export default function TermsPage() {
these Terms.
</p>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">Eligibility & Accounts</h2>
<p className="text-muted-foreground">
You may use the site and product only if you can form a binding
@@ -29,13 +31,13 @@ export default function TermsPage() {
</p>
</section>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">Acceptable Use</h2>
<p className="text-muted-foreground">
You agree not to misuse the site or product. Prohibited conduct
includes, without limitation:
</p>
<ul className="list-disc pl-5 space-y-2 text-muted-foreground">
<ul className="text-muted-foreground list-disc space-y-2 pl-5">
<li>Violating any applicable laws or regulations.</li>
<li>Infringing the rights of others or violating their privacy.</li>
<li>Attempting to interfere with or disrupt the services.</li>
@@ -46,7 +48,7 @@ export default function TermsPage() {
</ul>
</section>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">Anti-Spam Enforcement</h2>
<p className="text-muted-foreground">
To protect our community, we may suspend or block access for any
@@ -55,14 +57,14 @@ export default function TermsPage() {
may include immediate account suspension or termination.
</p>
<p className="text-muted-foreground">
Marketing communications sent via useSend must employ double
opt-in verification. Accounts that bypass double opt-in or misuse
our transactional mail API for promotional campaigns may be
suspended or terminated without notice.
Marketing communications sent via useSend must employ double opt-in
verification. Accounts that bypass double opt-in or misuse our
transactional mail API for promotional campaigns may be suspended or
terminated without notice.
</p>
</section>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">Intellectual Property</h2>
<p className="text-muted-foreground">
Content on the site, including trademarks, logos, text, and
@@ -72,7 +74,7 @@ export default function TermsPage() {
</p>
</section>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">ThirdParty Links</h2>
<p className="text-muted-foreground">
The site may contain links to thirdparty websites or services we do
@@ -80,7 +82,7 @@ export default function TermsPage() {
</p>
</section>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">Disclaimer</h2>
<p className="text-muted-foreground">
The site is provided on an "as is" and "as available" basis without
@@ -88,7 +90,7 @@ export default function TermsPage() {
</p>
</section>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">Limitation of Liability</h2>
<p className="text-muted-foreground">
To the fullest extent permitted by law, useSend shall not be liable
@@ -97,7 +99,7 @@ export default function TermsPage() {
</p>
</section>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">Indemnification</h2>
<p className="text-muted-foreground">
You agree to indemnify and hold harmless useSend from any claims,
@@ -106,7 +108,7 @@ export default function TermsPage() {
</p>
</section>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">Changes & Availability</h2>
<p className="text-muted-foreground">
We may modify these Terms and update the site or product at any
@@ -115,7 +117,7 @@ export default function TermsPage() {
</p>
</section>
<section className="space-y-3 mb-8">
<section className="mb-8 space-y-3">
<h2 className="text-xl font-medium">Governing Law</h2>
<p className="text-muted-foreground">
These Terms are governed by applicable laws without regard to
@@ -125,15 +127,23 @@ export default function TermsPage() {
</p>
</section>
<section className="space-y-3 mb-10">
<section className="mb-10 space-y-3">
<h2 className="text-xl font-medium">Contact</h2>
<p className="text-muted-foreground">
Questions about these Terms? Contact us at
<a href="mailto:hey@usesend.com" className="ml-1 underline decoration-dotted">hey@usesend.com</a>.
<a
href="mailto:hey@usesend.com"
className="ml-1 underline decoration-dotted"
>
hey@usesend.com
</a>
.
</p>
</section>
<p className="text-xs text-muted-foreground">Last updated: {new Date().toLocaleDateString()}</p>
<p className="text-muted-foreground text-xs">
Last updated: {new Date().toLocaleDateString()}
</p>
</div>
</main>
);

View File

@@ -1,14 +1,10 @@
import type { ReactNode } from "react";
import { SiteFooter } from "~/components/SiteFooter";
import { TopNav } from "~/components/TopNav";
import type { ReactNode } from 'react';
import { SiteFooter } from '~/components/SiteFooter';
import { TopNav } from '~/components/TopNav';
export default function UpdateLayout({
children,
}: {
children: ReactNode;
}) {
export default function UpdateLayout({ children }: { children: ReactNode }) {
return (
<main className="min-h-screen bg-background text-foreground">
<main className="bg-background text-foreground min-h-screen">
<TopNav />
<div className="mx-auto w-full max-w-3xl px-6 py-16">
<article className="space-y-8">{children}</article>

View File

@@ -1,6 +1,6 @@
import { Button } from "@usesend/ui/src/button";
import { CodeBlock } from "@usesend/ui/src/code-block";
import { LangToggle } from "./CodeLangToggle";
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";
@@ -82,34 +82,34 @@ if ($response === false) {
curl_close($ch);`;
export function CodeExample() {
const containerId = "code-example";
const containerId = 'code-example';
const languages = [
{
key: "ts",
label: "TypeScript",
kind: "ts",
shiki: "typescript" as const,
key: 'ts',
label: 'TypeScript',
kind: 'ts',
shiki: 'typescript' as const,
code: TS_CODE,
},
{
key: "py",
label: "Python",
kind: "py",
shiki: "python" as const,
key: 'py',
label: 'Python',
kind: 'py',
shiki: 'python' as const,
code: PY_CODE,
},
{
key: "go",
label: "Go",
kind: "go",
shiki: "go" as const,
key: 'go',
label: 'Go',
kind: 'go',
shiki: 'go' as const,
code: GO_CODE,
},
{
key: "php",
label: "PHP",
kind: "php",
shiki: "php" as const,
key: 'php',
label: 'PHP',
kind: 'php',
shiki: 'php' as const,
code: PHP_CODE,
},
];
@@ -118,17 +118,17 @@ export function CodeExample() {
<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">
<div className="text-primary mb-2 text-sm uppercase tracking-wider">
Developers
</div>
<p className="mt-1 text-xs sm:text-sm text-muted-foreground max-w-2xl mx-auto">
<p className="text-muted-foreground mx-auto mt-1 max-w-2xl text-xs sm:text-sm">
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">
<div className="text-muted-foreground mb-4 flex items-center justify-center gap-2 py-2 text-xs">
<LangToggle
containerId={containerId}
defaultLang="ts"
@@ -139,19 +139,19 @@ export function CodeExample() {
}))}
/>
</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">
<div className="bg-primary/20 rounded-[18px] p-1">
<div className="bg-primary/20 rounded-[14px] p-0.5 shadow-sm">
<div className="bg-background overflow-hidden rounded-xl">
{languages.map((l, idx) => (
<div
key={l.key}
data-lang-slot={l.key}
className={idx === 0 ? "block" : "hidden"}
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]"
className="rounded-[10px] p-4"
>
{l.code}
</CodeBlock>

View File

@@ -1,13 +1,13 @@
"use client";
'use client';
import { useEffect, useState } from "react";
import Image from "next/image";
import { Button } from "@usesend/ui/src/button";
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
kind: 'ts' | 'py' | string; // used for icon selection
};
export function LangToggle({
@@ -26,38 +26,36 @@ export function LangToggle({
if (!container) return;
const slots = Array.from(
container.querySelectorAll<HTMLElement>("[data-lang-slot]")
container.querySelectorAll<HTMLElement>('[data-lang-slot]'),
);
for (const el of slots) {
const key = el.getAttribute("data-lang-slot");
const key = el.getAttribute('data-lang-slot');
if (key === active) {
el.classList.remove("hidden");
el.classList.add("block");
el.classList.remove('hidden');
el.classList.add('block');
} else {
el.classList.add("hidden");
el.classList.remove("block");
el.classList.add('hidden');
el.classList.remove('block');
}
}
}, [active, containerId]);
return (
<div className="flex items-center gap-2 justify-center">
<div className="flex items-center justify-center gap-2">
{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")
'bg-transparent px-3 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}
<LangIcon kind={l.kind} className="mr-1 h-4 w-4" /> {l.label}
</span>
</Button>
))}
@@ -65,16 +63,27 @@ export function LangToggle({
);
}
function LangIcon({ kind, className = "h-4 w-4" }: { kind: string; className?: string }) {
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">
<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")
if (kind === 'ts')
return (
<Image
src="/typescript.svg"
@@ -86,7 +95,7 @@ function LangIcon({ kind, className = "h-4 w-4" }: { kind: string; className?: s
onError={() => setFailed(true)}
/>
);
if (kind === "py")
if (kind === 'py')
return (
<Image
src="/python.svg"
@@ -98,7 +107,7 @@ function LangIcon({ kind, className = "h-4 w-4" }: { kind: string; className?: s
onError={() => setFailed(true)}
/>
);
if (kind === "go")
if (kind === 'go')
return (
<Image
src="/go.svg"
@@ -110,7 +119,7 @@ function LangIcon({ kind, className = "h-4 w-4" }: { kind: string; className?: s
onError={() => setFailed(true)}
/>
);
if (kind === "php")
if (kind === 'php')
return (
<Image
src="/php.svg"
@@ -123,7 +132,12 @@ function LangIcon({ kind, className = "h-4 w-4" }: { kind: string; className?: s
/>
);
return (
<svg viewBox="0 0 24 24" aria-hidden="true" className={className} role="img">
<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>
);

View File

@@ -1,6 +1,6 @@
"use client";
'use client';
import Image from "next/image";
import Image from 'next/image';
type FeatureCardProps = {
title?: string;
@@ -21,33 +21,33 @@ export function FeatureCard({
imageSrc,
}: FeatureCardProps) {
return (
<div className="rounded-[18px] bg-primary/20 p-1 ">
<div className="h-full rounded-[14px] bg-primary/20 p-0.5 shadow-sm">
<div className="bg-background rounded-xl h-full flex flex-col">
<div className="relative w-full aspect-[16/9] rounded-t-xl overflow-hidden">
<div className="bg-primary/20 rounded-[18px] p-1">
<div className="bg-primary/20 h-full rounded-[14px] p-0.5 shadow-sm">
<div className="bg-background flex h-full flex-col rounded-xl">
<div className="relative aspect-[16/9] w-full overflow-hidden rounded-t-xl">
{imageLightSrc || imageDarkSrc ? (
<>
<Image
src={(imageLightSrc || imageDarkSrc)!}
alt={title || "Feature image"}
alt={title || 'Feature image'}
fill
className="object-cover dark:hidden rounded-t-xl"
className="rounded-t-xl object-cover dark:hidden"
priority={false}
/>
<Image
src={(imageDarkSrc || imageLightSrc)!}
alt={title || "Feature image"}
alt={title || 'Feature image'}
fill
className="object-cover hidden dark:block rounded-t-xl"
className="hidden rounded-t-xl object-cover dark:block"
priority={false}
/>
</>
) : imageSrc ? (
<Image
src={imageSrc}
alt={title || "Feature image"}
alt={title || 'Feature image'}
fill
className="object-cover rounded-t-xl"
className="rounded-t-xl object-cover"
priority={false}
/>
) : (
@@ -56,29 +56,29 @@ export function FeatureCard({
src="/hero-light.png"
alt="Feature image"
fill
className="object-cover dark:hidden rounded-t-xl"
className="rounded-t-xl object-cover dark:hidden"
priority={false}
/>
<Image
src="/hero-dark.png"
alt="Feature image"
fill
className="object-cover hidden dark:block rounded-t-xl"
className="hidden rounded-t-xl object-cover dark:block"
priority={false}
/>
</>
)}
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-12 sm:h-16 bg-gradient-to-b from-transparent via-background/60 to-background" />
<div className="via-background/60 to-background pointer-events-none absolute inset-x-0 bottom-0 h-12 bg-gradient-to-b from-transparent sm:h-16" />
</div>
<div className="p-5 flex-1 flex flex-col">
<h3 className="text-base sm:text-lg text-primary font-sans">
{title || ""}
<div className="flex flex-1 flex-col p-5">
<h3 className="text-primary font-sans text-base sm:text-lg">
{title || ''}
</h3>
{content ? (
<p className="mt-2 text-sm leading-relaxed">{content}</p>
<p className="mt-2 text-sm leading-relaxed">{content}</p>
) : (
<div className="mt-2 text-sm text-muted-foreground min-h-[1.5rem]"></div>
<div className="text-muted-foreground mt-2 min-h-[1.5rem] text-sm"></div>
)}
</div>
</div>

View File

@@ -1,4 +1,4 @@
"use client";
'use client';
export function FeatureCardPlain({
title,
@@ -7,19 +7,18 @@ export function FeatureCardPlain({
title?: string;
content?: string;
}) {
return (
<div className="rounded-[18px] bg-primary/20 p-1">
<div className="h-full rounded-[14px] bg-primary/20 p-0.5 shadow-sm">
<div className="bg-background rounded-xl h-full flex flex-col">
<div className="p-5 flex-1 flex flex-col">
<h3 className="text-base sm:text-lg text-primary font-sans">
{title || ""}
<div className="bg-primary/20 rounded-[18px] p-1">
<div className="bg-primary/20 h-full rounded-[14px] p-0.5 shadow-sm">
<div className="bg-background flex h-full flex-col rounded-xl">
<div className="flex flex-1 flex-col p-5">
<h3 className="text-primary font-sans text-base sm:text-lg">
{title || ''}
</h3>
{content ? (
<p className="mt-2 text-sm leading-relaxed">{content}</p>
) : (
<div className="mt-2 text-sm text-muted-foreground min-h-[1.5rem]"></div>
<div className="text-muted-foreground mt-2 min-h-[1.5rem] text-sm"></div>
)}
</div>
</div>

View File

@@ -1,6 +1,6 @@
import { Button } from "@usesend/ui/src/button";
import { Button } from '@usesend/ui/src/button';
const REPO = "usesend/usesend";
const REPO = 'usesend/usesend';
const REPO_URL = `https://github.com/${REPO}`;
const API_URL = `https://api.github.com/repos/${REPO}`;
const REVALIDATE_SECONDS = 60 * 60 * 24 * 7; // 7 days
@@ -8,15 +8,15 @@ const REVALIDATE_SECONDS = 60 * 60 * 24 * 7; // 7 days
function formatCompact(n: number): string {
if (n < 1000) return n.toLocaleString();
const units = [
{ v: 1_000_000_000, s: " B" },
{ v: 1_000_000, s: " M" },
{ v: 1_000, s: " K" },
{ v: 1_000_000_000, s: ' B' },
{ v: 1_000_000, s: ' M' },
{ v: 1_000, s: ' K' },
];
for (const u of units) {
if (n >= u.v) {
const num = n / u.v;
const rounded = Math.round(num * 10) / 10; // 1 decimal
const str = rounded.toFixed(1).replace(/\.0$/, "");
const str = rounded.toFixed(1).replace(/\.0$/, '');
return str + u.s;
}
}
@@ -25,9 +25,9 @@ function formatCompact(n: number): string {
export async function GitHubStarsButton() {
const headers: Record<string, string> = {
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "usesend-marketing",
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
'User-Agent': 'usesend-marketing',
};
if (process.env.GITHUB_TOKEN)
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
@@ -40,17 +40,17 @@ export async function GitHubStarsButton() {
});
if (res.ok) {
const json = (await res.json()) as { stargazers_count?: number };
if (typeof json.stargazers_count === "number")
if (typeof json.stargazers_count === 'number')
stars = json.stargazers_count;
}
} catch {
// ignore network errors; show placeholder
}
const formatted = stars == null ? "—" : formatCompact(stars);
const formatted = stars == null ? '—' : formatCompact(stars);
return (
<Button variant="outline" size="lg" className="px-4 gap-2">
<Button variant="outline" size="lg" className="gap-2 px-4">
<a
href={REPO_URL}
target="_blank"
@@ -60,7 +60,7 @@ export async function GitHubStarsButton() {
>
<GitHubIcon className="h-4 w-4" />
<span>GitHub</span>
<span className="rounded-md bg-muted px-1.5 py-0.5 text-xs tabular-nums text-muted-foreground">
<span className="bg-muted text-muted-foreground rounded-md px-1.5 py-0.5 text-xs tabular-nums">
{formatted}
</span>
</a>
@@ -68,7 +68,7 @@ export async function GitHubStarsButton() {
);
}
function GitHubIcon({ className = "" }: { className?: string }) {
function GitHubIcon({ className = '' }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,6 +1,6 @@
"use client";
'use client';
import React from "react";
import React from 'react';
type SliderProps = {
label: string;
@@ -19,35 +19,35 @@ function Slider({
min = 0,
max = 100000,
step = 500,
suffix = "",
suffix = '',
}: SliderProps) {
const id = React.useId();
const [dragging, setDragging] = React.useState(false);
const percent = Math.max(
0,
Math.min(100, ((value - min) / (max - min)) * 100)
Math.min(100, ((value - min) / (max - min)) * 100),
);
React.useEffect(() => {
if (!dragging) return;
const stop = () => setDragging(false);
window.addEventListener("mouseup", stop);
window.addEventListener("touchend", stop);
window.addEventListener("pointerup", stop);
window.addEventListener('mouseup', stop);
window.addEventListener('touchend', stop);
window.addEventListener('pointerup', stop);
return () => {
window.removeEventListener("mouseup", stop);
window.removeEventListener("touchend", stop);
window.removeEventListener("pointerup", stop);
window.removeEventListener('mouseup', stop);
window.removeEventListener('touchend', stop);
window.removeEventListener('pointerup', stop);
};
}, [dragging]);
return (
<div className="flex flex-col sm:flex-row gap-3 sm:items-center">
<div className="w-full sm:w-56 md:w-72 shrink-0">
<label htmlFor={id} className="text-sm font-medium block">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<div className="w-full shrink-0 sm:w-56 md:w-72">
<label htmlFor={id} className="block text-sm font-medium">
{label}
</label>
<div className="mt-1 text-xs sm:text-sm text-muted-foreground tabular-nums truncate">
<div className="text-muted-foreground mt-1 truncate text-xs tabular-nums sm:text-sm">
{value.toLocaleString()} {suffix}
</div>
</div>
@@ -63,7 +63,7 @@ function Slider({
onMouseDown={() => setDragging(true)}
onTouchStart={() => setDragging(true)}
onPointerDown={() => setDragging(true)}
className="w-full accent-primary"
className="accent-primary w-full"
aria-label={label}
aria-valuetext={`${value.toLocaleString()} ${suffix}`}
/>
@@ -72,7 +72,7 @@ function Slider({
className="pointer-events-none absolute -top-9 left-0 -translate-x-1/2"
style={{ left: `${percent}%` }}
>
<div className="rounded-md bg-foreground px-2 py-1 text-[11px] font-medium text-background tabular-nums shadow whitespace-nowrap">
<div className="bg-foreground text-background whitespace-nowrap rounded-md px-2 py-1 text-[11px] font-medium tabular-nums shadow">
{value.toLocaleString()} {suffix}
</div>
</div>
@@ -98,15 +98,15 @@ export function PricingCalculator() {
const totalDue = Math.max(subtotal, MINIMUM_SPEND);
return (
<div className="rounded-[18px] bg-primary/20 p-1">
<div className="rounded-[14px] bg-primary/20 p-0.5 shadow-sm">
<div className="bg-primary/20 rounded-[18px] p-1">
<div className="bg-primary/20 rounded-[14px] p-0.5 shadow-sm">
<div className="bg-background rounded-xl p-5 pb-10">
<div className="flex flex-col gap-6">
<div className="text-center">
<div className="text-sm uppercase tracking-wider text-primary">
<div className="text-primary text-sm uppercase tracking-wider">
Pricing Calculator
</div>
<p className="mt-1 text-xs sm:text-sm text-muted-foreground">
<p className="text-muted-foreground mt-1 text-xs sm:text-sm">
Drag the sliders to estimate your monthly cost.
</p>
</div>
@@ -132,38 +132,38 @@ export function PricingCalculator() {
/>
</div>
<div className="mt-2 grid grid-cols-1 sm:grid-cols-3 gap-4 items-center">
<div className="rounded-lg border border-primary/30 p-4">
<div className="text-xs text-muted-foreground">Marketing</div>
<div className="mt-2 grid grid-cols-1 items-center gap-4 sm:grid-cols-3">
<div className="border-primary/30 rounded-lg border p-4">
<div className="text-muted-foreground text-xs">Marketing</div>
<div className="text-lg font-medium">
${marketingCost.toFixed(2)}
</div>
<div className="text-xs text-muted-foreground">
<div className="text-muted-foreground text-xs">
@ ${MARKETING_RATE.toFixed(4)} each
</div>
</div>
<div className="rounded-lg border border-primary/30 p-4">
<div className="text-xs text-muted-foreground">
<div className="border-primary/30 rounded-lg border p-4">
<div className="text-muted-foreground text-xs">
Transactional
</div>
<div className="text-lg font-medium">
${transactionalCost.toFixed(2)}
</div>
<div className="text-xs text-muted-foreground">
<div className="text-muted-foreground text-xs">
@ ${TRANSACTIONAL_RATE.toFixed(4)} each
</div>
</div>
<div className="rounded-lg border border-primary/30 p-4 bg-primary/10">
<div className="text-xs text-muted-foreground">
<div className="border-primary/30 bg-primary/10 rounded-lg border p-4">
<div className="text-muted-foreground text-xs">
Estimated Total
</div>
<div className="text-3xl text-primary font-semibold">
<div className="text-primary text-3xl font-semibold">
${totalDue.toFixed(2)}
</div>
<div className="text-xs text-muted-foreground">
<div className="text-muted-foreground text-xs">
{subtotal < MINIMUM_SPEND
? "Minimum $10 applies"
: "before taxes"}
? 'Minimum $10 applies'
: 'before taxes'}
</div>
</div>
</div>

View File

@@ -1,16 +1,16 @@
import Image from "next/image";
import Link from "next/link";
import Image from 'next/image';
import Link from 'next/link';
// Replaced StatusBadge with external status badge image
const REPO = "usesend/usesend";
const REPO = 'usesend/usesend';
const REPO_URL = `https://github.com/${REPO}`;
const APP_URL = "https://app.usesend.com";
const APP_URL = 'https://app.usesend.com';
export function SiteFooter() {
return (
<footer className="py-10 border-t border-border">
<footer className="border-border border-t py-10">
<div className="mx-auto max-w-6xl px-6">
<div className="flex flex-col sm:flex-row sm:items-start gap-4">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start">
<div className="flex items-center gap-2 sm:w-56">
<Image
src="/logo-squircle.png"
@@ -21,13 +21,13 @@ export function SiteFooter() {
<span className="text-primary font-mono">useSend</span>
</div>
<div className="sm:ml-auto flex items-start gap-4">
<div className="grid grid-cols-2 sm:grid-cols-3 gap-x-12 gap-y-2 text-sm">
<div className="flex items-start gap-4 sm:ml-auto">
<div className="grid grid-cols-2 gap-x-12 gap-y-2 text-sm sm:grid-cols-3">
<div>
<div className="text-xs uppercase tracking-wider mb-2">
<div className="mb-2 text-xs uppercase tracking-wider">
Product
</div>
<ul className="space-y-2 text-muted-foreground">
<ul className="text-muted-foreground space-y-2">
<li>
<a
href={APP_URL}
@@ -62,10 +62,10 @@ export function SiteFooter() {
</div>
<div>
<div className="text-xs uppercase tracking-wider mb-2">
<div className="mb-2 text-xs uppercase tracking-wider">
Contact
</div>
<ul className="space-y-2 text-muted-foreground">
<ul className="text-muted-foreground space-y-2">
<li>
<a
href="mailto:hey@usesend.com"
@@ -118,10 +118,10 @@ export function SiteFooter() {
</div>
<div>
<div className="text-xs uppercase tracking-wider mb-2">
<div className="mb-2 text-xs uppercase tracking-wider">
Company
</div>
<ul className="space-y-2 text-muted-foreground">
<ul className="text-muted-foreground space-y-2">
<li>
<Link
href="/privacy"
@@ -160,7 +160,7 @@ export function SiteFooter() {
</div>
</div>
<div className="mt-6 text-xs text-muted-foreground mx-auto text-center">
<div className="text-muted-foreground mx-auto mt-6 text-center text-xs">
© {new Date().getFullYear()} useSend. All rights reserved.
</div>
</div>

View File

@@ -1,31 +1,38 @@
"use client";
'use client';
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useState } from "react";
import { Button } from "@usesend/ui/src/button";
import Image from 'next/image';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useState } from 'react';
import { Button } from '@usesend/ui/src/button';
const REPO = "usesend/usesend";
const REPO = 'usesend/usesend';
const REPO_URL = `https://github.com/${REPO}`;
const APP_URL = "https://app.usesend.com";
const APP_URL = 'https://app.usesend.com';
export function TopNav() {
const [open, setOpen] = useState(false);
const pathname = usePathname();
const isHome = pathname === "/";
const pricingHref = isHome ? "#pricing" : "/#pricing";
const isHome = pathname === '/';
const pricingHref = isHome ? '#pricing' : '/#pricing';
return (
<header className="py-4 border-b border-border sticky top-0 z-20 backdrop-blur supports-[backdrop-filter]:bg-sidebar-background/80">
<div className="mx-auto max-w-6xl px-6 flex items-center justify-between gap-4 text-sm">
<Link href="/" className="flex items-center gap-2 group">
<Image src="/logo-squircle.png" alt="useSend" width={24} height={24} />
<span className="text-primary font-mono text-[16px] group-hover:opacity-90">useSend</span>
<header className="border-border supports-[backdrop-filter]:bg-sidebar-background/80 sticky top-0 z-20 border-b py-4 backdrop-blur">
<div className="mx-auto flex max-w-6xl items-center justify-between gap-4 px-6 text-sm">
<Link href="/" className="group flex items-center gap-2">
<Image
src="/logo-squircle.png"
alt="useSend"
width={24}
height={24}
/>
<span className="text-primary font-mono text-[16px] group-hover:opacity-90">
useSend
</span>
</Link>
{/* Desktop nav */}
<nav className="hidden sm:flex items-center gap-4 text-muted-foreground">
<nav className="text-muted-foreground hidden items-center gap-4 sm:flex">
<Link href={pricingHref} className="hover:text-foreground">
Pricing
</Link>
@@ -55,14 +62,29 @@ export function TopNav() {
{/* Mobile hamburger */}
<button
aria-label="Open menu"
className="sm:hidden inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:text-foreground hover:bg-accent focus:outline-none focus:ring-2 focus:ring-border"
className="text-muted-foreground hover:text-foreground hover:bg-accent focus:ring-border inline-flex items-center justify-center rounded-md p-2 focus:outline-none focus:ring-2 sm:hidden"
onClick={() => setOpen((v) => !v)}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="h-6 w-6">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
className="h-6 w-6"
>
{open ? (
<path d="M6 18 18 6M6 6l12 12" strokeLinecap="round" strokeLinejoin="round" />
<path
d="M6 18 18 6M6 6l12 12"
strokeLinecap="round"
strokeLinejoin="round"
/>
) : (
<path d="M3 6h18M3 12h18M3 18h18" strokeLinecap="round" strokeLinejoin="round" />
<path
d="M3 6h18M3 12h18M3 18h18"
strokeLinecap="round"
strokeLinejoin="round"
/>
)}
</svg>
</button>
@@ -70,16 +92,20 @@ export function TopNav() {
{/* Mobile menu panel */}
{open ? (
<div className="sm:hidden border-t border-border bg-sidebar-background/95 backdrop-blur">
<div className="mx-auto max-w-6xl px-6 py-3 flex flex-col gap-2">
<Link href={pricingHref} className="py-2 text-muted-foreground hover:text-foreground" onClick={() => setOpen(false)}>
<div className="border-border bg-sidebar-background/95 border-t backdrop-blur sm:hidden">
<div className="mx-auto flex max-w-6xl flex-col gap-2 px-6 py-3">
<Link
href={pricingHref}
className="text-muted-foreground hover:text-foreground py-2"
onClick={() => setOpen(false)}
>
Pricing
</Link>
<a
href="https://docs.usesend.com"
target="_blank"
rel="noopener noreferrer"
className="py-2 text-muted-foreground hover:text-foreground"
className="text-muted-foreground hover:text-foreground py-2"
onClick={() => setOpen(false)}
>
Docs
@@ -88,14 +114,19 @@ export function TopNav() {
href={REPO_URL}
target="_blank"
rel="noopener noreferrer"
className="py-2 text-muted-foreground hover:text-foreground"
className="text-muted-foreground hover:text-foreground py-2"
onClick={() => setOpen(false)}
>
GitHub
</a>
<div className="pt-2">
<Button className="w-full">
<a href={APP_URL} target="_blank" rel="noopener noreferrer" onClick={() => setOpen(false)}>
<a
href={APP_URL}
target="_blank"
rel="noopener noreferrer"
onClick={() => setOpen(false)}
>
Get started
</a>
</Button>

View File

@@ -1,31 +1,31 @@
import type { MDXComponents } from "mdx/types";
import type { MDXComponents } from 'mdx/types';
const components = {
h1: ({ children }) => (
<h1 className="text-3xl font-semibold tracking-wide font-sans text-primary">
<h1 className="text-primary font-sans text-3xl font-semibold tracking-wide">
{children}
</h1>
),
h2: ({ children }) => (
<h2 className="text-xl font-semibold tracking-wide font-sans text-primary">
<h2 className="text-primary font-sans text-xl font-semibold tracking-wide">
{children}
</h2>
),
h3: ({ children }) => (
<h3 className="text-lg font-medium tracking-wide font-sans">{children}</h3>
<h3 className="font-sans text-lg font-medium tracking-wide">{children}</h3>
),
p: ({ children }) => (
<p className="text-base font-normal tracking-wide leading-relaxed font-sans">
<p className="font-sans text-base font-normal leading-relaxed tracking-wide">
{children}
</p>
),
ul: ({ children }) => (
<ul className="list-disc list-inside font-sans pl-4 space-y-1">
<ul className="list-inside list-disc space-y-1 pl-4 font-sans">
{children}
</ul>
),
a: ({ children, href }) => (
<a href={href} className=" text-primary-light">
<a href={href} className="text-primary-light">
{children}
</a>
),

View File

@@ -1,13 +1,12 @@
import { type Config } from "tailwindcss";
import sharedConfig from "@usesend/tailwind-config/tailwind.config";
import path from "path";
import { type Config } from 'tailwindcss';
import sharedConfig from '@usesend/tailwind-config/tailwind.config';
import path from 'path';
export default {
...sharedConfig,
content: [
"./src/**/*.tsx",
`${path.join(require.resolve("@usesend/ui"), "..")}/**/*.{ts,tsx}`,
`${path.join(require.resolve("@usesend/email-editor"), "..")}/**/*.{ts,tsx}`,
'./src/**/*.tsx',
`${path.join(require.resolve('@usesend/ui'), '..')}/**/*.{ts,tsx}`,
`${path.join(require.resolve('@usesend/email-editor'), '..')}/**/*.{ts,tsx}`,
],
} satisfies Config;