add pricing calculator (#214)
BIN
apps/marketing/public/editor-dark.webp
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
apps/marketing/public/editor-light.webp
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
apps/marketing/public/emails-search-dark.webp
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
apps/marketing/public/emails-search-light.webp
Normal file
After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 1.1 MiB |
BIN
apps/marketing/public/hero-dark.webp
Normal file
After Width: | Height: | Size: 82 KiB |
Before Width: | Height: | Size: 1.2 MiB |
BIN
apps/marketing/public/hero-light.webp
Normal file
After Width: | Height: | Size: 76 KiB |
@@ -1,13 +1,15 @@
|
||||
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 { CodeBlock } from "@usesend/ui/src/code-block";
|
||||
|
||||
const REPO = "unsend-dev/unsend";
|
||||
const REPO = "usesend/usesend";
|
||||
const REPO_URL = `https://github.com/${REPO}`;
|
||||
const APP_URL = "https://app.usesend.com";
|
||||
|
||||
@@ -21,7 +23,7 @@ export default function Page() {
|
||||
<CodeExample />
|
||||
<Pricing />
|
||||
<About />
|
||||
<Footer />
|
||||
<SiteFooter />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -61,7 +63,7 @@ function Hero() {
|
||||
<div className="rounded-[18px] bg-primary/10 p-1 sm:p-1 ">
|
||||
<div className="rounded-2xl bg-primary/20 p-1 sm:p-1 ">
|
||||
<Image
|
||||
src="/hero-light.png"
|
||||
src="/hero-light.webp"
|
||||
alt="useSend product hero"
|
||||
width={3456}
|
||||
height={1914}
|
||||
@@ -71,7 +73,7 @@ function Hero() {
|
||||
priority={false}
|
||||
/>
|
||||
<Image
|
||||
src="/hero-dark.png"
|
||||
src="/hero-dark.webp"
|
||||
alt="useSend product hero"
|
||||
width={3456}
|
||||
height={1914}
|
||||
@@ -222,14 +224,16 @@ function Features() {
|
||||
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.",
|
||||
imageSrc: "", // add an image like "/analytics.png"
|
||||
imageLightSrc: "/emails-search-light.webp",
|
||||
imageDarkSrc: "/emails-search-dark.webp",
|
||||
},
|
||||
{
|
||||
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.",
|
||||
imageSrc: "", // add an image like "/editor.png"
|
||||
imageLightSrc: "/editor-light.webp",
|
||||
imageDarkSrc: "/editor-dark.webp",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -271,7 +275,8 @@ function Features() {
|
||||
key={f.key}
|
||||
title={f.title}
|
||||
content={f.content}
|
||||
imageSrc={f.imageSrc}
|
||||
imageLightSrc={f.imageLightSrc}
|
||||
imageDarkSrc={f.imageDarkSrc}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -370,7 +375,7 @@ function Pricing() {
|
||||
PRICING
|
||||
</div>
|
||||
<p className="mt-1 text-xs sm:text-sm text-muted-foreground max-w-2xl mx-auto">
|
||||
pay for what you use, not for storing contacts
|
||||
pay for what you use, the most affordable email platform
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -388,6 +393,10 @@ function Pricing() {
|
||||
perks={paidPerks}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<PricingCalculator />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -469,50 +478,7 @@ function About() {
|
||||
|
||||
// FAQ section removed per request
|
||||
|
||||
function Footer() {
|
||||
return (
|
||||
<footer className="py-10 border-t border-border">
|
||||
<div className="mx-auto max-w-6xl px-6 flex flex-col sm:flex-row items-center justify-between gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<Image
|
||||
src="/logo-squircle.png"
|
||||
alt="useSend"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<span className="text-primary font-mono">useSend</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="#features" className="hover:text-foreground">
|
||||
Features
|
||||
</Link>
|
||||
<Link href="/privacy" className="hover:text-foreground">
|
||||
Privacy
|
||||
</Link>
|
||||
<Link href="/terms" className="hover:text-foreground">
|
||||
Terms
|
||||
</Link>
|
||||
<a
|
||||
href={REPO_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href={APP_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground"
|
||||
>
|
||||
Get Started
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
// Footer moved to ~/components/SiteFooter
|
||||
|
||||
// Minimal inline icons (stroke-based, sleek)
|
||||
function CheckIcon({ className = "" }: { className?: string }) {
|
||||
|
@@ -2,21 +2,47 @@
|
||||
|
||||
import Image from "next/image";
|
||||
|
||||
type FeatureCardProps = {
|
||||
title?: string;
|
||||
content?: string;
|
||||
imageLightSrc?: string;
|
||||
imageDarkSrc?: string;
|
||||
/**
|
||||
* Deprecated: prefer imageLightSrc and imageDarkSrc
|
||||
*/
|
||||
imageSrc?: string;
|
||||
};
|
||||
|
||||
export function FeatureCard({
|
||||
title,
|
||||
content,
|
||||
imageLightSrc,
|
||||
imageDarkSrc,
|
||||
imageSrc,
|
||||
}: {
|
||||
title?: string;
|
||||
content?: string;
|
||||
imageSrc?: string;
|
||||
}) {
|
||||
}: 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">
|
||||
{imageSrc ? (
|
||||
<div className="relative w-full aspect-[16/9] rounded-t-xl overflow-hidden">
|
||||
{imageLightSrc || imageDarkSrc ? (
|
||||
<>
|
||||
<Image
|
||||
src={(imageLightSrc || imageDarkSrc)!}
|
||||
alt={title || "Feature image"}
|
||||
fill
|
||||
className="object-cover dark:hidden rounded-t-xl"
|
||||
priority={false}
|
||||
/>
|
||||
<Image
|
||||
src={(imageDarkSrc || imageLightSrc)!}
|
||||
alt={title || "Feature image"}
|
||||
fill
|
||||
className="object-cover hidden dark:block rounded-t-xl"
|
||||
priority={false}
|
||||
/>
|
||||
</>
|
||||
) : imageSrc ? (
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={title || "Feature image"}
|
||||
@@ -42,6 +68,7 @@ export function FeatureCard({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<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>
|
||||
|
||||
<div className="p-5 flex-1 flex flex-col">
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
|
||||
const REPO = "unsend-dev/unsend";
|
||||
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
|
||||
|
177
apps/marketing/src/components/PricingCalculator.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
type SliderProps = {
|
||||
label: string;
|
||||
value: number;
|
||||
onChange: (v: number) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
suffix?: string;
|
||||
};
|
||||
|
||||
function Slider({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
min = 0,
|
||||
max = 100000,
|
||||
step = 500,
|
||||
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)
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!dragging) return;
|
||||
const stop = () => setDragging(false);
|
||||
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);
|
||||
};
|
||||
}, [dragging]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-40 sm:w-56 md:w-72 shrink-0">
|
||||
<label htmlFor={id} className="text-sm font-medium block">
|
||||
{label}
|
||||
</label>
|
||||
<div className="mt-1 text-xs sm:text-sm text-muted-foreground tabular-nums truncate">
|
||||
{value.toLocaleString()} {suffix}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex-1">
|
||||
<input
|
||||
id={id}
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
onMouseDown={() => setDragging(true)}
|
||||
onTouchStart={() => setDragging(true)}
|
||||
onPointerDown={() => setDragging(true)}
|
||||
className="w-full accent-primary"
|
||||
aria-label={label}
|
||||
aria-valuetext={`${value.toLocaleString()} ${suffix}`}
|
||||
/>
|
||||
{dragging && (
|
||||
<div
|
||||
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">
|
||||
{value.toLocaleString()} {suffix}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PricingCalculator() {
|
||||
// Rates from pricing copy
|
||||
const MARKETING_RATE = 0.001; // $ per marketing email
|
||||
const TRANSACTIONAL_RATE = 0.0004; // $ per transactional email
|
||||
const MINIMUM_SPEND = 10; // $ minimum monthly spend
|
||||
|
||||
// Defaults chosen to total $10: 8000*$0.001 + 5000*$0.0004 = 10
|
||||
const [marketing, setMarketing] = React.useState<number>(5000);
|
||||
const [transactional, setTransactional] = React.useState<number>(12500);
|
||||
|
||||
const marketingCost = marketing * MARKETING_RATE;
|
||||
const transactionalCost = transactional * TRANSACTIONAL_RATE;
|
||||
const subtotal = marketingCost + transactionalCost;
|
||||
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-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">
|
||||
Pricing Calculator
|
||||
</div>
|
||||
<p className="mt-1 text-xs sm:text-sm text-muted-foreground">
|
||||
Drag the sliders to estimate your monthly cost.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
<Slider
|
||||
label="Marketing emails / month"
|
||||
value={marketing}
|
||||
onChange={setMarketing}
|
||||
min={0}
|
||||
max={3000000}
|
||||
step={500}
|
||||
suffix="emails"
|
||||
/>
|
||||
<Slider
|
||||
label="Transactional emails / month"
|
||||
value={transactional}
|
||||
onChange={setTransactional}
|
||||
min={0}
|
||||
max={3000000}
|
||||
step={500}
|
||||
suffix="emails"
|
||||
/>
|
||||
</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="text-lg font-medium">
|
||||
${marketingCost.toFixed(2)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
@ ${MARKETING_RATE.toFixed(4)} each
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-primary/30 p-4">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Transactional
|
||||
</div>
|
||||
<div className="text-lg font-medium">
|
||||
${transactionalCost.toFixed(2)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
@ ${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">
|
||||
Estimated Total
|
||||
</div>
|
||||
<div className="text-3xl text-primary font-semibold">
|
||||
${totalDue.toFixed(2)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{subtotal < MINIMUM_SPEND
|
||||
? "Minimum $10 applies"
|
||||
: "before taxes"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PricingCalculator;
|
90
apps/marketing/src/components/SiteFooter.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { StatusBadge } from "~/components/StatusBadge";
|
||||
|
||||
const REPO = "usesend/usesend";
|
||||
const REPO_URL = `https://github.com/${REPO}`;
|
||||
const APP_URL = "https://app.usesend.com";
|
||||
|
||||
export function SiteFooter() {
|
||||
return (
|
||||
<footer className="py-10 border-t border-border">
|
||||
<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 items-center gap-2 sm:w-56">
|
||||
<Image src="/logo-squircle.png" alt="useSend" width={24} height={24} />
|
||||
<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-6 gap-y-2 text-sm">
|
||||
<div>
|
||||
<div className="text-xs uppercase tracking-wider text-muted-foreground mb-2">Product</div>
|
||||
<ul className="space-y-2 text-muted-foreground">
|
||||
<li>
|
||||
<a href={APP_URL} target="_blank" rel="noopener noreferrer" className="hover:text-foreground">
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={REPO_URL} target="_blank" rel="noopener noreferrer" className="hover:text-foreground">
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://docs.usesend.com" target="_blank" rel="noopener noreferrer" className="hover:text-foreground">
|
||||
Docs
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-xs uppercase tracking-wider text-muted-foreground mb-2">Contact</div>
|
||||
<ul className="space-y-2 text-muted-foreground">
|
||||
<li>
|
||||
<a href="mailto:hey@usesend.com" className="hover:text-foreground">Email</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://x.com/useSend_com" target="_blank" rel="noopener noreferrer" className="hover:text-foreground">
|
||||
X (Twitter)
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.linkedin.com/company/use-send/" target="_blank" rel="noopener noreferrer" className="hover:text-foreground">
|
||||
LinkedIn
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://bsky.app/profile/usesend.com" target="_blank" rel="noopener noreferrer" className="hover:text-foreground">
|
||||
Bluesky
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-xs uppercase tracking-wider text-muted-foreground mb-2">Company</div>
|
||||
<ul className="space-y-2 text-muted-foreground">
|
||||
<li>
|
||||
<Link href="/privacy" className="hover:text-foreground">Privacy</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/terms" className="hover:text-foreground">Terms</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StatusBadge />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-xs text-muted-foreground mx-auto text-center">
|
||||
© {new Date().getFullYear()} useSend. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
131
apps/marketing/src/components/StatusBadge.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
type StatusState = "operational" | "degraded" | "down" | "unknown";
|
||||
|
||||
// Best-effort fetcher for Uptime Kuma public status page JSON.
|
||||
// Falls back gracefully if the endpoint or CORS is not available.
|
||||
async function fetchUptimeStatus(baseUrl: string): Promise<StatusState> {
|
||||
const candidates = [
|
||||
"/api/status-page/heartbeat/default", // specific uptime-kuma status page slug
|
||||
];
|
||||
|
||||
for (const path of candidates) {
|
||||
try {
|
||||
const res = await fetch(baseUrl.replace(/\/$/, "") + path, {
|
||||
// Always fetch from the browser; avoid caching too aggressively
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!res.ok) continue;
|
||||
const data: any = await res.json().catch(() => null);
|
||||
if (!data) continue;
|
||||
|
||||
// Heuristics across possible Kuma payloads
|
||||
// 1) overallStatus or status fields
|
||||
const overall = (data.overallStatus || data.status || "")
|
||||
.toString()
|
||||
.toLowerCase();
|
||||
if (
|
||||
overall.includes("up") ||
|
||||
overall.includes("ok") ||
|
||||
overall.includes("oper")
|
||||
)
|
||||
return "operational";
|
||||
if (overall.includes("degrad") || overall.includes("partial"))
|
||||
return "degraded";
|
||||
if (
|
||||
overall.includes("down") ||
|
||||
overall.includes("outage") ||
|
||||
overall.includes("incident")
|
||||
)
|
||||
return "down";
|
||||
|
||||
// 2) heartbeat style: if any monitor is down
|
||||
if (Array.isArray(data.monitors) && Array.isArray(data.heartbeatList)) {
|
||||
// If any lastHeartbeatStatus === 0 (down) => down
|
||||
const isAnyDown = Object.values<any>(data.heartbeatList).some(
|
||||
(arr: any[]) =>
|
||||
Array.isArray(arr) && arr.some((hb) => hb?.status === 0)
|
||||
);
|
||||
if (isAnyDown) return "down";
|
||||
return "operational";
|
||||
}
|
||||
|
||||
// 3) Generic boolean hints
|
||||
if (typeof data.allUp === "boolean")
|
||||
return data.allUp ? "operational" : "down";
|
||||
|
||||
// Unknown but successful response
|
||||
return "unknown";
|
||||
} catch {
|
||||
// Try next candidate
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
export function StatusBadge({
|
||||
baseUrl = "https://status.usesend.com",
|
||||
}: {
|
||||
baseUrl?: string;
|
||||
}) {
|
||||
const [status, setStatus] = useState<StatusState>("unknown");
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
const load = async () => {
|
||||
const s = await fetchUptimeStatus(baseUrl);
|
||||
if (mounted) setStatus(s);
|
||||
};
|
||||
load();
|
||||
const id = setInterval(load, 60_000); // refresh every 60s
|
||||
return () => {
|
||||
mounted = false;
|
||||
clearInterval(id);
|
||||
};
|
||||
}, [baseUrl]);
|
||||
|
||||
const dotClass = useMemo(() => {
|
||||
switch (status) {
|
||||
case "operational":
|
||||
return "bg-green";
|
||||
case "degraded":
|
||||
return "bg-yellow";
|
||||
case "down":
|
||||
return "bg-red";
|
||||
default:
|
||||
return "bg-muted-foreground";
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
const label = useMemo(() => {
|
||||
switch (status) {
|
||||
case "operational":
|
||||
return "operational";
|
||||
case "degraded":
|
||||
return "degraded";
|
||||
case "down":
|
||||
return "outage";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
return (
|
||||
<a
|
||||
href={baseUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/10 px-3 py-1 text-xs font-medium text-primary hover:bg-primary/20 transition-colors"
|
||||
aria-label={`Service status: ${label}`}
|
||||
title={`Status: ${label}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-2 w-2 rounded-full ${dotClass} shadow-[0_0_0_2px] shadow-background`}
|
||||
/>
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
}
|
@@ -6,7 +6,7 @@ import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
|
||||
const REPO = "unsend-dev/unsend";
|
||||
const REPO = "usesend/usesend";
|
||||
const REPO_URL = `https://github.com/${REPO}`;
|
||||
const APP_URL = "https://app.usesend.com";
|
||||
|
||||
|
@@ -9,7 +9,7 @@
|
||||
--foreground: 234 16% 35%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--card-foreground: 234 16% 35%;
|
||||
|
||||
--popover: 220 2% 96%;
|
||||
--popover-foreground: 234 16% 35%;
|
||||
@@ -71,7 +71,7 @@
|
||||
--foreground: 226 64% 88%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--card-foreground: 226 64% 88%;
|
||||
|
||||
--popover: 240 21% 15%;
|
||||
--popover-foreground: 226 64% 88%;
|
||||
|