add rebrand landing page (#211)
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
|
||||
export function FeatureCard({
|
||||
title,
|
||||
content,
|
||||
imageSrc,
|
||||
}: {
|
||||
title?: string;
|
||||
content?: string;
|
||||
imageSrc?: 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="relative w-full aspect-[16/9] rounded-t-xl">
|
||||
{imageSrc ? (
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={title || "Feature image"}
|
||||
fill
|
||||
className="object-cover rounded-t-xl"
|
||||
priority={false}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Image
|
||||
src="/hero-light.png"
|
||||
alt="Feature image"
|
||||
fill
|
||||
className="object-cover dark:hidden rounded-t-xl"
|
||||
priority={false}
|
||||
/>
|
||||
<Image
|
||||
src="/hero-dark.png"
|
||||
alt="Feature image"
|
||||
fill
|
||||
className="object-cover hidden dark:block rounded-t-xl"
|
||||
priority={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-5 flex-1 flex flex-col">
|
||||
<h3 className="text-base sm:text-lg text-primary font-sans">
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
export function FeatureCardPlain({
|
||||
title,
|
||||
content,
|
||||
}: {
|
||||
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 || ""}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
|
||||
const REPO = "unsend-dev/unsend";
|
||||
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
|
||||
|
||||
export function GitHubStarsButton() {
|
||||
const [stars, setStars] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
async function load() {
|
||||
try {
|
||||
const res = await fetch(API_URL, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const json = await res.json();
|
||||
if (!cancelled && typeof json.stargazers_count === "number") {
|
||||
setStars(json.stargazers_count);
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore network errors; keep placeholder
|
||||
}
|
||||
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" },
|
||||
];
|
||||
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$/, "");
|
||||
return str + u.s;
|
||||
}
|
||||
load();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
return n.toLocaleString();
|
||||
}
|
||||
|
||||
const formatted = stars?.toLocaleString() ?? "—";
|
||||
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",
|
||||
};
|
||||
if (process.env.GITHUB_TOKEN)
|
||||
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
||||
|
||||
let stars: number | null = null;
|
||||
try {
|
||||
const res = await fetch(API_URL, {
|
||||
headers,
|
||||
next: { revalidate: REVALIDATE_SECONDS },
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = (await res.json()) as { 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);
|
||||
|
||||
return (
|
||||
<Button variant="outline" size="lg" className="px-4 gap-2">
|
||||
@@ -48,7 +59,10 @@ export function GitHubStarsButton() {
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<GitHubIcon className="h-4 w-4" />
|
||||
<span>Star on GitHub</span>
|
||||
<span>GitHub</span>
|
||||
<span className="rounded-md bg-muted px-1.5 py-0.5 text-xs tabular-nums text-muted-foreground">
|
||||
{formatted}
|
||||
</span>
|
||||
</a>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"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";
|
||||
|
||||
const REPO = "unsend-dev/unsend";
|
||||
const REPO_URL = `https://github.com/${REPO}`;
|
||||
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";
|
||||
|
||||
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>
|
||||
</Link>
|
||||
|
||||
{/* Desktop nav */}
|
||||
<nav className="hidden sm:flex items-center gap-4 text-muted-foreground">
|
||||
<Link href={pricingHref} className="hover:text-foreground">
|
||||
Pricing
|
||||
</Link>
|
||||
<a
|
||||
href="https://docs.usesend.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground"
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
<a
|
||||
href={REPO_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<Button size="sm" className="ml-2">
|
||||
<a href={APP_URL} target="_blank" rel="noopener noreferrer">
|
||||
Get started
|
||||
</a>
|
||||
</Button>
|
||||
</nav>
|
||||
|
||||
{/* 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"
|
||||
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">
|
||||
{open ? (
|
||||
<path d="M6 18 18 6M6 6l12 12" strokeLinecap="round" strokeLinejoin="round" />
|
||||
) : (
|
||||
<path d="M3 6h18M3 12h18M3 18h18" strokeLinecap="round" strokeLinejoin="round" />
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 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)}>
|
||||
Pricing
|
||||
</Link>
|
||||
<a
|
||||
href="https://docs.usesend.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="py-2 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
<a
|
||||
href={REPO_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="py-2 text-muted-foreground hover:text-foreground"
|
||||
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)}>
|
||||
Get started
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user