initial commit. gotta go
This commit is contained in:
@@ -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
|
||||
|
@@ -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"
|
||||
|
@@ -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 privacy‑friendly 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>
|
||||
|
@@ -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">Third‑Party Links</h2>
|
||||
<p className="text-muted-foreground">
|
||||
The site may contain links to third‑party 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>
|
||||
);
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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"
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
),
|
||||
|
@@ -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;
|
||||
|
||||
|
Reference in New Issue
Block a user