add rebrand landing page (#211)
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
You are a Staff Engineer and an Expert in ReactJS, NextJS, JavaScript, TypeScript, HTML, CSS, NodeJS, Prisma, Postgres, and modern UI/UX frameworks (e.g., TailwindCSS, Shadcn, Radix). You are also great at scalling things. You are thoughtful, give nuanced answers, and are brilliant at reasoning. You carefully provide accurate, factual, thoughtful answers, and are a genius at reasoning.
|
||||
|
||||
- Follow the user’s requirements carefully & to the letter.
|
||||
- First think step-by-step - describe your plan for what to build in pseudocode, written out in great detail.
|
||||
- Always write correct, best practice, bug free, fully functional and working code also it should be aligned to listed rules down below at Code Implementation Guidelines .
|
||||
- Focus on easy and readability code, over being performant.
|
||||
- Fully implement all requested functionality.
|
||||
- Leave NO todo’s, placeholders or missing pieces.
|
||||
- Ensure code is complete! Verify thoroughly finalised.
|
||||
- Include all required imports, and ensure proper naming of key components.
|
||||
- Be concise Minimize any other prose.
|
||||
- If you think there might not be a correct answer, you say so.
|
||||
- If you do not know the answer, say so, instead of guessing.
|
BIN
apps/marketing/public/hero-dark.png
Normal file
BIN
apps/marketing/public/hero-dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 MiB |
BIN
apps/marketing/public/hero-light.png
Normal file
BIN
apps/marketing/public/hero-light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 MiB |
@@ -16,9 +16,35 @@ const jetbrainsMono = JetBrains_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "useSend - Open source email platform",
|
||||
description: "Open source email platform for everyone",
|
||||
title: "useSend – Open source email platform",
|
||||
description:
|
||||
"Open source email platform for everyone: SMTP, API, editor, analytics.",
|
||||
icons: [{ rel: "icon", url: "/favicon.ico" }],
|
||||
metadataBase: new URL("https://usesend.com"),
|
||||
openGraph: {
|
||||
title: "useSend – Open source email platform",
|
||||
description:
|
||||
"Open source email platform for everyone: SMTP, API, editor, analytics.",
|
||||
url: "https://usesend.com",
|
||||
siteName: "useSend",
|
||||
images: [
|
||||
{
|
||||
url: "/logo-squircle.png",
|
||||
width: 512,
|
||||
height: 512,
|
||||
alt: "useSend",
|
||||
},
|
||||
],
|
||||
locale: "en_US",
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "useSend – Open source email platform",
|
||||
description:
|
||||
"Open source email platform for everyone: SMTP, API, editor, analytics.",
|
||||
images: ["/logo-squircle.png"],
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -27,11 +53,17 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning className="bg-sidebar-background">
|
||||
<html lang="en" suppressHydrationWarning className="scroll-smooth bg-background">
|
||||
<body
|
||||
className={`font-sans ${inter.variable} ${jetbrainsMono.variable} bg-sidebar-background`}
|
||||
className={`font-mono ${inter.variable} ${jetbrainsMono.variable} bg-background`}
|
||||
>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
{/* System theme with isolated storage to avoid stale overrides */}
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
storageKey="marketing-theme"
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
|
@@ -1,27 +1,532 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
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 { CodeBlock } from "@usesend/ui/src/code-block";
|
||||
|
||||
const REPO = "unsend-dev/unsend";
|
||||
const REPO_URL = `https://github.com/${REPO}`;
|
||||
const APP_URL = "https://app.usesend.com";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<main className="min-h-screen flex items-center justify-center p-6">
|
||||
<div className="max-w-2xl text-center space-y-4">
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/logo-squircle.png"
|
||||
alt="useSend logo"
|
||||
width={64}
|
||||
height={64}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<h1 className="text-xl font-mono font-medium text-blue">useSend</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Open source email platform for everyone
|
||||
</p>
|
||||
<div className="mt-6 flex items-center justify-center gap-3">
|
||||
<GitHubStarsButton />
|
||||
</div>
|
||||
</div>
|
||||
<main className="min-h-screen bg-sidebar-background text-foreground">
|
||||
<TopNav />
|
||||
<Hero />
|
||||
<TrustedBy />
|
||||
<Features />
|
||||
<CodeExample />
|
||||
<Pricing />
|
||||
<About />
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
// (Removed unused SectionHeading component)
|
||||
|
||||
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">
|
||||
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.{" "}
|
||||
<span className="text-primary font-normal">
|
||||
Pay only for what you send
|
||||
</span>{" "}
|
||||
and not for storing contacts. Open source and self-hostable.
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex flex-col sm:flex-row items-center justify-center gap-3">
|
||||
<Button size="lg" className="px-6">
|
||||
<a href={APP_URL} target="_blank" rel="noopener noreferrer">
|
||||
Get started
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
<GitHubStarsButton />
|
||||
</div>
|
||||
|
||||
<p className="mt-3 text-center text-xs text-muted-foreground">
|
||||
Open source • Self-host in minutes • Free tier
|
||||
</p>
|
||||
|
||||
<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 ">
|
||||
<Image
|
||||
src="/hero-light.png"
|
||||
alt="useSend product hero"
|
||||
width={3456}
|
||||
height={1914}
|
||||
className="w-full h-auto rounded-xl block dark:hidden"
|
||||
sizes="(min-width: 1024px) 900px, 100vw"
|
||||
loading="eager"
|
||||
priority={false}
|
||||
/>
|
||||
<Image
|
||||
src="/hero-dark.png"
|
||||
alt="useSend product hero"
|
||||
width={3456}
|
||||
height={1914}
|
||||
className="w-full h-auto rounded-xl hidden dark:block"
|
||||
sizes="(min-width: 1024px) 900px, 100vw"
|
||||
loading="eager"
|
||||
priority={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// TopNav moved to a dedicated client component in ~/components/TopNav
|
||||
|
||||
function TrustedBy() {
|
||||
const featured = [
|
||||
{
|
||||
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",
|
||||
image:
|
||||
"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",
|
||||
image:
|
||||
"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",
|
||||
image:
|
||||
"https://pbs.twimg.com/profile_images/1698447401781022720/b0DZSc_D_400x400.jpg",
|
||||
},
|
||||
{
|
||||
quote: "Thank you for making useSend!",
|
||||
author: "Andras Bacsai",
|
||||
company: "coolify.io",
|
||||
image:
|
||||
"https://pbs.twimg.com/profile_images/1884210412524027905/jW4NB4rx_400x400.jpg",
|
||||
},
|
||||
{
|
||||
quote: "I KNOW WHAT TO DO",
|
||||
author: "VicVijayakumar",
|
||||
company: "onetimefax.com",
|
||||
image:
|
||||
"https://pbs.twimg.com/profile_images/1665351804685524995/W4BpDx5Z_400x400.jpg",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<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">
|
||||
<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">
|
||||
{featured.map((t) => (
|
||||
<figure
|
||||
key={t.author + t.company}
|
||||
className="rounded-xl border border-primary/30 p-5 h-full"
|
||||
>
|
||||
<blockquote className="text-sm sm:text-base font-light font-sans ">
|
||||
{t.quote}
|
||||
</blockquote>
|
||||
<div className="mt-5 flex items-center gap-3">
|
||||
<Image
|
||||
src={t.image}
|
||||
alt={`${t.author} avatar`}
|
||||
width={32}
|
||||
height={32}
|
||||
className=" rounded-md border-2 border-primary/50"
|
||||
/>
|
||||
<figcaption className="text-sm">
|
||||
<span className="font-medium">{t.author}</span>
|
||||
<a
|
||||
href={`https://${t.company}`}
|
||||
target="_blank"
|
||||
className="text-muted-foreground hover:text-primary-light"
|
||||
>
|
||||
{" "}
|
||||
— {t.company}
|
||||
</a>{" "}
|
||||
</figcaption>
|
||||
</div>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bottom: 3 multi-line testimonials (same style as top) */}
|
||||
<div className="mt-4 grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
{quick.map((t) => (
|
||||
<figure
|
||||
key={t.author + t.company}
|
||||
className="rounded-xl border border-primary/30 p-5 h-full"
|
||||
>
|
||||
<blockquote className="text-sm sm:text-base font-light font-sans leading-relaxed">
|
||||
{t.quote}
|
||||
</blockquote>
|
||||
<div className="mt-5 flex items-center gap-3">
|
||||
<Image
|
||||
src={t.image}
|
||||
alt={`${t.author} avatar`}
|
||||
width={32}
|
||||
height={32}
|
||||
className=" rounded-md border-2 border-primary/50"
|
||||
/>
|
||||
<figcaption className="text-sm">
|
||||
<span className="font-medium">{t.author}</span>
|
||||
<a
|
||||
href={`https://${t.company}`}
|
||||
target="_blank"
|
||||
className="text-muted-foreground hover:text-primary-light"
|
||||
>
|
||||
{" "}
|
||||
— {t.company}
|
||||
</a>
|
||||
</figcaption>
|
||||
</div>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Features() {
|
||||
// Top: 2 cards (with image area) — Analytics, Editor
|
||||
const top = [
|
||||
{
|
||||
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.",
|
||||
imageSrc: "", // add an image like "/analytics.png"
|
||||
},
|
||||
{
|
||||
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"
|
||||
},
|
||||
];
|
||||
|
||||
// Bottom: 3 cards (no images) — Contact Management, Suppression List, SMTP Relay Service
|
||||
const bottom = [
|
||||
{
|
||||
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.",
|
||||
},
|
||||
{
|
||||
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.",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<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">
|
||||
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">
|
||||
{top.map((f) => (
|
||||
<FeatureCard
|
||||
key={f.key}
|
||||
title={f.title}
|
||||
content={f.content}
|
||||
imageSrc={f.imageSrc}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bottom row: 3 cards without images */}
|
||||
<div className="mt-6 grid grid-cols-1 sm:grid-cols-3 gap-6">
|
||||
{bottom.map((f) => (
|
||||
<FeatureCardPlain key={f.key} title={f.title} content={f.content} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function CodeExample() {
|
||||
const code = `import { Unsend } from "@unsend/sdk";
|
||||
|
||||
const unsend = new Unsend({ apiKey: process.env.UNSEND_API_KEY! });
|
||||
|
||||
await unsend.emails.send({
|
||||
from: "hi@example.com",
|
||||
to: "you@company.com",
|
||||
subject: "Welcome to useSend",
|
||||
template: "welcome", // or html/text
|
||||
data: { name: "Ada" },
|
||||
});`;
|
||||
|
||||
return (
|
||||
<section className="py-16 sm:py-20">
|
||||
<div className="mx-auto max-w-6xl px-6">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-sm uppercase tracking-wider text-primary">
|
||||
Developers
|
||||
</div>
|
||||
<p className="mt-1 text-xs sm:text-sm text-muted-foreground max-w-2xl mx-auto">
|
||||
Typed SDKs and simple APIs, so you can focus on product not
|
||||
plumbing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 overflow-hidden">
|
||||
<div className=" py-2 text-xs text-muted-foreground">TypeScript</div>
|
||||
<div className="rounded-[18px] bg-primary/20 p-1">
|
||||
<div className="rounded-[14px] bg-primary/20 p-0.5 shadow-sm">
|
||||
<div className="bg-background rounded-xl overflow-hidden">
|
||||
<CodeBlock
|
||||
lang="typescript"
|
||||
children={code}
|
||||
className="p-4 rounded-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex items-center justify-center gap-3">
|
||||
<Button size="lg" className="px-6">
|
||||
<a
|
||||
href="https://docs.usesend.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read the docs
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
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",
|
||||
];
|
||||
|
||||
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",
|
||||
];
|
||||
|
||||
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">
|
||||
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
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<PricingCard
|
||||
title="Free"
|
||||
price="$0"
|
||||
note="per month"
|
||||
perks={freePerks}
|
||||
/>
|
||||
<PricingCard
|
||||
title="Paid"
|
||||
price="$10"
|
||||
note="minimum usage per month"
|
||||
perks={paidPerks}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
type PricingCardProps = {
|
||||
title: string;
|
||||
price: string;
|
||||
note: string;
|
||||
perks: string[];
|
||||
};
|
||||
|
||||
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">
|
||||
{perks.map((perk) => (
|
||||
<li key={perk} className="flex items-start gap-2">
|
||||
<CheckIcon className="w-4 h-4 mt-0.5 text-primary" />
|
||||
<span>{perk}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-auto pt-6">
|
||||
<Button className="">
|
||||
<a
|
||||
href="https://app.usesend.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Get started
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function About() {
|
||||
return (
|
||||
<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">
|
||||
About
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 max-w-3xl mx-auto text-sm sm:text-base space-y-4">
|
||||
<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
|
||||
way to send emails reliably and cheaply with a great dashboard.
|
||||
</p>
|
||||
<p>
|
||||
useSend is bootstrapped and funded by the cloud offering and
|
||||
sponsors. If you self host useSend, please consider{" "}
|
||||
<a
|
||||
href="https://github.com/sponsors/KMKoushik"
|
||||
target="_blank"
|
||||
className="text-primary-light"
|
||||
>
|
||||
sponsoring us
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
|
||||
// Minimal inline icons (stroke-based, sleek)
|
||||
function CheckIcon({ className = "" }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M20 6 9 17l-5-5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
197
apps/marketing/src/app/privacy/page.tsx
Normal file
197
apps/marketing/src/app/privacy/page.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
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.",
|
||||
};
|
||||
|
||||
export default function PrivacyPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-sidebar-background text-foreground">
|
||||
<TopNav />
|
||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||
<h1 className="text-3xl font-semibold tracking-tight mb-6">
|
||||
Privacy Policy
|
||||
</h1>
|
||||
<p className="text-muted-foreground mb-8">
|
||||
This Privacy Policy explains how we collect, use, and share
|
||||
information when you visit or interact with the useSend marketing
|
||||
website at usesend.com. It also summarizes the limited information we
|
||||
process when you sign up for our product and receive transactional or
|
||||
occasional marketing emails.
|
||||
</p>
|
||||
|
||||
<section className="space-y-3 mb-8">
|
||||
<h2 className="text-xl font-medium">Who We Are</h2>
|
||||
<p className="text-muted-foreground">
|
||||
useSend ("we", "us") operates the marketing website at
|
||||
<span className="mx-1 font-mono">usesend.com</span>. The marketing
|
||||
site is hosted on Vercel. Our application is hosted on Railway. We
|
||||
are the controller of the information described in this policy for
|
||||
the marketing site. If you have questions about this policy or your
|
||||
data, contact us at
|
||||
<a
|
||||
href="mailto:hey@usesend.com"
|
||||
className="ml-1 underline decoration-dotted"
|
||||
>
|
||||
hey@usesend.com
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 mb-8">
|
||||
<h2 className="text-xl font-medium">What We Collect</h2>
|
||||
<ul className="list-disc pl-5 space-y-2 text-muted-foreground">
|
||||
<li>
|
||||
<span className="text-foreground">
|
||||
Usage and device data (marketing site):
|
||||
</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
|
||||
use cookies for tracking. Data is aggregated and not used to
|
||||
identify you.
|
||||
</li>
|
||||
<li>
|
||||
<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.
|
||||
</li>
|
||||
<li>
|
||||
<span className="text-foreground">
|
||||
Account and email data (product):
|
||||
</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
|
||||
the link in those emails.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 mb-8">
|
||||
<h2 className="text-xl font-medium">How We Use Information</h2>
|
||||
<ul className="list-disc pl-5 space-y-2 text-muted-foreground">
|
||||
<li>Operate, secure, and maintain the marketing site and app.</li>
|
||||
<li>
|
||||
Understand aggregated usage to improve performance and content.
|
||||
</li>
|
||||
<li>
|
||||
Deliver transactional emails related to your account or use.
|
||||
</li>
|
||||
<li>Send occasional marketing emails to subscribers who opt in.</li>
|
||||
<li>Comply with legal obligations and enforce our terms.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 mb-8">
|
||||
<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
|
||||
interests to operate and secure our services and to measure
|
||||
aggregated site usage, and on your consent for marketing emails. We
|
||||
may rely on contract and legal obligation where relevant.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 mb-8">
|
||||
<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">
|
||||
<li>
|
||||
<span className="text-foreground">Hosting:</span> Vercel
|
||||
(marketing site) and Railway (application) for serving content,
|
||||
networking, and security.
|
||||
</li>
|
||||
<li>
|
||||
<span className="text-foreground">Analytics:</span> Simple
|
||||
Analytics for aggregated, privacy‑friendly usage metrics on the
|
||||
marketing site.
|
||||
</li>
|
||||
<li>
|
||||
<span className="text-foreground">Email delivery:</span> We send
|
||||
transactional emails and, for subscribers who opt in, occasional
|
||||
marketing emails.
|
||||
</li>
|
||||
</ul>
|
||||
<p className="text-muted-foreground">
|
||||
We do not sell your personal information. We may disclose
|
||||
information if required by law or to protect our rights, users, or
|
||||
the public.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 mb-8">
|
||||
<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
|
||||
purposes described in this policy, including security, analytics,
|
||||
and legal compliance. Aggregated analytics do not identify
|
||||
individuals.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 mb-8">
|
||||
<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
|
||||
of residence. Where required, we implement appropriate safeguards
|
||||
for cross‑border transfers.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 mb-8">
|
||||
<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,
|
||||
delete, or export your information; to object to or restrict certain
|
||||
processing; and to withdraw consent where processing is based on
|
||||
consent. To exercise these rights, contact us using the details on
|
||||
our website. We may ask you to verify your identity before acting on
|
||||
a request.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 mb-8">
|
||||
<h2 className="text-xl font-medium">Contact</h2>
|
||||
<p className="text-muted-foreground">
|
||||
For privacy requests or questions, email us at
|
||||
<a
|
||||
href="mailto:hey@usesend.com"
|
||||
className="ml-1 underline decoration-dotted"
|
||||
>
|
||||
hey@usesend.com
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 mb-8">
|
||||
<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
|
||||
collect personal information from children.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 mb-10">
|
||||
<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
|
||||
below reflects the most recent changes.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Last updated: {new Date().toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
124
apps/marketing/src/app/terms/page.tsx
Normal file
124
apps/marketing/src/app/terms/page.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
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.",
|
||||
};
|
||||
|
||||
export default function TermsPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-sidebar-background text-foreground">
|
||||
<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>
|
||||
<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.
|
||||
By accessing or using our site or product, you agree to be bound by
|
||||
these Terms.
|
||||
</p>
|
||||
|
||||
<section className="space-y-3 mb-8">
|
||||
<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
|
||||
contract with useSend and are not barred from doing so under any
|
||||
applicable laws. You are responsible for maintaining the security of
|
||||
your account credentials and for all activity under your account.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 mb-8">
|
||||
<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">
|
||||
<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>
|
||||
<li>
|
||||
Uploading or transmitting malicious code, spam, or prohibited
|
||||
content.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 mb-8">
|
||||
<h2 className="text-xl font-medium">Intellectual Property</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Content on the site, including trademarks, logos, text, and
|
||||
graphics, is owned by or licensed to useSend and protected by
|
||||
intellectual property laws. You may not use our marks without our
|
||||
prior written permission.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 mb-8">
|
||||
<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
|
||||
not control. We are not responsible for their content or practices.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 mb-8">
|
||||
<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
|
||||
warranties of any kind, express or implied.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 mb-8">
|
||||
<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
|
||||
for any indirect, incidental, special, consequential or punitive
|
||||
damages, or any loss of profits or revenues.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 mb-8">
|
||||
<h2 className="text-xl font-medium">Indemnification</h2>
|
||||
<p className="text-muted-foreground">
|
||||
You agree to indemnify and hold harmless useSend from any claims,
|
||||
damages, liabilities, and expenses arising out of your use of the
|
||||
site or product or your violation of these Terms.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 mb-8">
|
||||
<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
|
||||
time. Changes are effective when posted. We may suspend or
|
||||
discontinue the site or product in whole or in part.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 mb-8">
|
||||
<h2 className="text-xl font-medium">Governing Law</h2>
|
||||
<p className="text-muted-foreground">
|
||||
These Terms are governed by applicable laws without regard to
|
||||
conflict‑of‑law principles. Where required, disputes will be subject
|
||||
to the jurisdiction of competent courts in your place of residence
|
||||
or as otherwise mandated by law.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 mb-10">
|
||||
<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>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<p className="text-xs text-muted-foreground">Last updated: {new Date().toLocaleDateString()}</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
61
apps/marketing/src/components/FeatureCard.tsx
Normal file
61
apps/marketing/src/components/FeatureCard.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
28
apps/marketing/src/components/FeatureCardPlain.tsx
Normal file
28
apps/marketing/src/components/FeatureCardPlain.tsx
Normal file
@@ -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>
|
||||
);
|
||||
|
108
apps/marketing/src/components/TopNav.tsx
Normal file
108
apps/marketing/src/components/TopNav.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
253
apps/web/prisma/seed_dashboard.sql
Normal file
253
apps/web/prisma/seed_dashboard.sql
Normal file
@@ -0,0 +1,253 @@
|
||||
-- Seed data for a photogenic dashboard screenshot
|
||||
-- Postgres SQL compatible with the Prisma schema in apps/web/prisma/schema.prisma
|
||||
-- Usage:
|
||||
-- psql "$DATABASE_URL" -f apps/web/prisma/seed_dashboard.sql
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1) Create a team (allow duplicates for demo runs)
|
||||
INSERT INTO "Team" ("name", "plan", "isActive", "apiRateLimit", "createdAt", "updatedAt")
|
||||
VALUES ('Acme Inc', 'BASIC'::"Plan", TRUE, 10, NOW(), NOW());
|
||||
|
||||
-- 2) Ensure a verified domain for that team (upsert by unique name)
|
||||
INSERT INTO "Domain" (
|
||||
"name", "teamId", "status", "region", "clickTracking", "openTracking",
|
||||
"publicKey", "dkimSelector", "dmarcAdded", "createdAt", "updatedAt"
|
||||
)
|
||||
SELECT
|
||||
'mail.acme.test', id, 'SUCCESS'::"DomainStatus", 'us-east-1', TRUE, TRUE,
|
||||
'PUBLIC_KEY_SAMPLE', 'usesend', TRUE, NOW(), NOW()
|
||||
FROM "Team"
|
||||
WHERE "name" = 'Acme Inc'
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
ON CONFLICT ("name") DO UPDATE
|
||||
SET "updatedAt" = NOW();
|
||||
|
||||
-- 3) Cumulated totals to power headline KPIs (idempotent)
|
||||
INSERT INTO "CumulatedMetrics" ("teamId", "domainId", "delivered", "hardBounced", "complained")
|
||||
SELECT d."teamId", d.id, 125000, 750, 180
|
||||
FROM "Domain" d
|
||||
WHERE d."name" = 'mail.acme.test'
|
||||
ON CONFLICT ("teamId", "domainId") DO UPDATE
|
||||
SET "delivered" = EXCLUDED."delivered",
|
||||
"hardBounced" = EXCLUDED."hardBounced",
|
||||
"complained" = EXCLUDED."complained";
|
||||
|
||||
-- 4) 14 days of daily usage (TRANSACTIONAL) with internally consistent metrics
|
||||
INSERT INTO "DailyEmailUsage" (
|
||||
"teamId", "domainId", "date", "type",
|
||||
"sent", "delivered", "opened", "clicked",
|
||||
"bounced", "complained", "hardBounced",
|
||||
"createdAt", "updatedAt"
|
||||
)
|
||||
SELECT
|
||||
d."teamId",
|
||||
d.id AS "domainId",
|
||||
TO_CHAR((CURRENT_DATE - i)::date, 'YYYY-MM-DD') AS date,
|
||||
'TRANSACTIONAL'::"EmailUsageType" AS type,
|
||||
s.sent,
|
||||
s.delivered,
|
||||
s.opened,
|
||||
s.clicked,
|
||||
s.bounced,
|
||||
s.complained,
|
||||
s.hardBounced,
|
||||
NOW() - (i || ' days')::interval AS createdAt,
|
||||
NOW() - (i || ' days')::interval AS updatedAt
|
||||
FROM (
|
||||
SELECT id, "teamId" FROM "Domain" WHERE "name" = 'mail.acme.test' LIMIT 1
|
||||
) d,
|
||||
generate_series(0, 13) AS i,
|
||||
LATERAL (
|
||||
WITH season AS (
|
||||
SELECT
|
||||
CASE EXTRACT(DOW FROM (CURRENT_DATE - i))
|
||||
WHEN 0 THEN 0.80 -- Sunday
|
||||
WHEN 6 THEN 0.85 -- Saturday
|
||||
ELSE 1.00 -- Weekday
|
||||
END AS dow_factor,
|
||||
(1.0 + 0.06 * sin(2*pi() * ((13 - i)::float / 7.0))) AS wave
|
||||
), base AS (
|
||||
SELECT
|
||||
GREATEST(700, round((900 * dow_factor * wave) + 30*random()))::int AS sent_base
|
||||
FROM season
|
||||
), parts AS (
|
||||
SELECT
|
||||
sent_base AS sent,
|
||||
(0.006 + random()*0.007) AS bounce_frac
|
||||
FROM base
|
||||
)
|
||||
SELECT
|
||||
sent,
|
||||
(sent - floor(sent*bounce_frac)::int) AS delivered,
|
||||
floor((sent - floor(sent*bounce_frac)::int) * (0.60 + random()*0.15))::int AS opened,
|
||||
LEAST(
|
||||
floor((sent - floor(sent*bounce_frac)::int) * (0.18 + random()*0.12))::int,
|
||||
floor((sent - floor(sent*bounce_frac)::int) * 0.95)::int
|
||||
) AS clicked,
|
||||
floor(sent*bounce_frac)::int AS bounced,
|
||||
CASE WHEN random() < ((sent - floor(sent*bounce_frac)::int) * (0.0001 + random()*0.0002))
|
||||
THEN 1 ELSE 0 END AS complained,
|
||||
floor(floor(sent*bounce_frac)::int * (0.60 + random()*0.25))::int AS hardBounced
|
||||
FROM parts
|
||||
) s
|
||||
ON CONFLICT ("teamId", "domainId", "date", "type") DO NOTHING;
|
||||
|
||||
-- 5) 14 days of daily usage (MARKETING) with consistent metrics
|
||||
INSERT INTO "DailyEmailUsage" (
|
||||
"teamId", "domainId", "date", "type",
|
||||
"sent", "delivered", "opened", "clicked",
|
||||
"bounced", "complained", "hardBounced",
|
||||
"createdAt", "updatedAt"
|
||||
)
|
||||
SELECT
|
||||
d."teamId",
|
||||
d.id AS "domainId",
|
||||
TO_CHAR((CURRENT_DATE - i)::date, 'YYYY-MM-DD') AS date,
|
||||
'MARKETING'::"EmailUsageType" AS type,
|
||||
s.sent,
|
||||
s.delivered,
|
||||
s.opened,
|
||||
s.clicked,
|
||||
s.bounced,
|
||||
s.complained,
|
||||
s.hardBounced,
|
||||
NOW() - (i || ' days')::interval AS createdAt,
|
||||
NOW() - (i || ' days')::interval AS updatedAt
|
||||
FROM (
|
||||
SELECT id, "teamId" FROM "Domain" WHERE "name" = 'mail.acme.test' LIMIT 1
|
||||
) d,
|
||||
generate_series(0, 13) AS i,
|
||||
LATERAL (
|
||||
WITH season AS (
|
||||
SELECT
|
||||
CASE EXTRACT(DOW FROM (CURRENT_DATE - i))
|
||||
WHEN 0 THEN 0.75 -- Sunday
|
||||
WHEN 6 THEN 0.85 -- Saturday
|
||||
ELSE 1.00
|
||||
END AS dow_factor,
|
||||
(1.0 + 0.08 * sin(2*pi() * ((13 - i)::float / 7.0) + 0.6)) AS wave
|
||||
), base AS (
|
||||
SELECT
|
||||
GREATEST(500, round((700 * dow_factor * wave) + 40*random()))::int AS sent_base
|
||||
FROM season
|
||||
), parts AS (
|
||||
SELECT
|
||||
sent_base AS sent,
|
||||
(0.008 + random()*0.010) AS bounce_frac
|
||||
FROM base
|
||||
)
|
||||
SELECT
|
||||
sent,
|
||||
(sent - floor(sent*bounce_frac)::int) AS delivered,
|
||||
floor((sent - floor(sent*bounce_frac)::int) * (0.47 + random()*0.18))::int AS opened,
|
||||
LEAST(
|
||||
floor((sent - floor(sent*bounce_frac)::int) * (0.14 + random()*0.10))::int,
|
||||
floor((sent - floor(sent*bounce_frac)::int) * 0.90)::int
|
||||
) AS clicked,
|
||||
floor(sent*bounce_frac)::int AS bounced,
|
||||
CASE WHEN random() < ((sent - floor(sent*bounce_frac)::int) * (0.00005 + random()*0.00020))
|
||||
THEN 1 ELSE 0 END AS complained,
|
||||
floor(floor(sent*bounce_frac)::int * (0.60 + random()*0.25))::int AS hardBounced
|
||||
FROM parts
|
||||
) s
|
||||
ON CONFLICT ("teamId", "domainId", "date", "type") DO NOTHING;
|
||||
|
||||
-- 6) A recent campaign with healthy open/click, realistic bounce/complaint rates
|
||||
INSERT INTO "Campaign" (
|
||||
id, "name", "teamId", "from", cc, bcc, "replyTo", "domainId", subject, "previewText",
|
||||
html, content, "contactBookId", total, sent, delivered, opened, clicked, unsubscribed,
|
||||
bounced, "hardBounced", complained, status, "createdAt", "updatedAt"
|
||||
)
|
||||
SELECT
|
||||
('cmp_' || substr(md5(random()::text), 1, 12)) AS id,
|
||||
'August Promo – Back to Business' AS name,
|
||||
d."teamId" AS teamId,
|
||||
'Acme <noreply@mail.acme.test>' AS "from",
|
||||
ARRAY[]::text[] AS cc,
|
||||
ARRAY[]::text[] AS bcc,
|
||||
ARRAY['support@acme.test']::text[] AS "replyTo",
|
||||
d.id AS "domainId",
|
||||
'Save 30% on Pro' AS subject,
|
||||
'Limited-time offer for power users' AS "previewText",
|
||||
'<h1>Upgrade to Pro</h1><p>Unlock advanced features.</p>' AS html,
|
||||
NULL AS content,
|
||||
NULL AS "contactBookId",
|
||||
25000 AS total,
|
||||
24800 AS sent,
|
||||
24500 AS delivered,
|
||||
13600 AS opened,
|
||||
6200 AS clicked,
|
||||
90 AS unsubscribed,
|
||||
300 AS bounced,
|
||||
220 AS "hardBounced",
|
||||
5 AS complained,
|
||||
'SENT'::"CampaignStatus" AS status,
|
||||
NOW() - interval '2 days' AS "createdAt",
|
||||
NOW() - interval '1 days' AS "updatedAt"
|
||||
FROM "Domain" d
|
||||
JOIN "Team" t2 ON t2.id = d."teamId"
|
||||
WHERE d."name" = 'mail.acme.test'
|
||||
ORDER BY d.id DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- 7) A handful of recent emails (varied statuses) to make lists look alive
|
||||
DO $$
|
||||
DECLARE
|
||||
v_team_id INT;
|
||||
v_domain_id INT;
|
||||
BEGIN
|
||||
SELECT t.id, d.id INTO v_team_id, v_domain_id
|
||||
FROM "Team" t
|
||||
JOIN "Domain" d ON d."teamId" = t.id
|
||||
WHERE d."name" = 'mail.acme.test'
|
||||
ORDER BY d.id DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- delivered/opened
|
||||
INSERT INTO "Email" (
|
||||
id, "sesEmailId", "from", "to", "replyTo", cc, bcc, subject, text, html,
|
||||
"latestStatus", "teamId", "domainId", "createdAt", "updatedAt"
|
||||
) VALUES (
|
||||
('eml_' || substr(md5(random()::text), 1, 12)), NULL,
|
||||
'Acme <noreply@mail.acme.test>', ARRAY['user1@example.com'], ARRAY['support@acme.test'], ARRAY[]::text[], ARRAY[]::text[],
|
||||
'Welcome to Acme', 'Plaintext welcome', '<p>Welcome!</p>', 'OPENED'::"EmailStatus",
|
||||
v_team_id, v_domain_id, NOW() - interval '4 hours', NOW() - interval '3 hours'
|
||||
);
|
||||
|
||||
-- clicked
|
||||
INSERT INTO "Email" (
|
||||
id, "sesEmailId", "from", "to", "replyTo", cc, bcc, subject, text, html,
|
||||
"latestStatus", "teamId", "domainId", "createdAt", "updatedAt"
|
||||
) VALUES (
|
||||
('eml_' || substr(md5(random()::text), 1, 12)), NULL,
|
||||
'Acme <noreply@mail.acme.test>', ARRAY['user2@example.com'], ARRAY['support@acme.test'], ARRAY[]::text[], ARRAY[]::text[],
|
||||
'Get Started Guide', NULL, '<p>Click to learn more</p>', 'CLICKED'::"EmailStatus",
|
||||
v_team_id, v_domain_id, NOW() - interval '2 hours', NOW() - interval '1 hour'
|
||||
);
|
||||
|
||||
-- bounced
|
||||
INSERT INTO "Email" (
|
||||
id, "sesEmailId", "from", "to", "replyTo", cc, bcc, subject, text, html,
|
||||
"latestStatus", "teamId", "domainId", "createdAt", "updatedAt"
|
||||
) VALUES (
|
||||
('eml_' || substr(md5(random()::text), 1, 12)), NULL,
|
||||
'Acme <noreply@mail.acme.test>', ARRAY['bad@invalid.test'], ARRAY['support@acme.test'], ARRAY[]::text[], ARRAY[]::text[],
|
||||
'Delivery failed notice', NULL, '<p>Delivery failed</p>', 'BOUNCED'::"EmailStatus",
|
||||
v_team_id, v_domain_id, NOW() - interval '6 hours', NOW() - interval '5 hours'
|
||||
);
|
||||
|
||||
-- complained
|
||||
INSERT INTO "Email" (
|
||||
id, "sesEmailId", "from", "to", "replyTo", cc, bcc, subject, text, html,
|
||||
"latestStatus", "teamId", "domainId", "createdAt", "updatedAt"
|
||||
) VALUES (
|
||||
('eml_' || substr(md5(random()::text), 1, 12)), NULL,
|
||||
'Acme <noreply@mail.acme.test>', ARRAY['too.sensitive@example.com'], ARRAY['support@acme.test'], ARRAY[]::text[], ARRAY[]::text[],
|
||||
'Feedback request', 'We value your feedback', '<p>Please reply</p>', 'COMPLAINED'::"EmailStatus",
|
||||
v_team_id, v_domain_id, NOW() - interval '9 hours', NOW() - interval '8 hours'
|
||||
);
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
@@ -2,6 +2,7 @@ import React from "react";
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
Rectangle,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
@@ -22,6 +23,37 @@ interface EmailChartProps {
|
||||
domain: string | null;
|
||||
}
|
||||
|
||||
const STACK_ORDER: string[] = [
|
||||
"delivered",
|
||||
"bounced",
|
||||
"complained",
|
||||
"opened",
|
||||
"clicked",
|
||||
] as const;
|
||||
|
||||
type StackKey = (typeof STACK_ORDER)[number];
|
||||
|
||||
function createRoundedTopShape(currentKey: StackKey) {
|
||||
const currentIndex = STACK_ORDER.indexOf(currentKey);
|
||||
return (props: any) => {
|
||||
const payload = props.payload as
|
||||
| Partial<Record<StackKey, number>>
|
||||
| undefined;
|
||||
let hasAbove = false;
|
||||
for (let i = currentIndex + 1; i < STACK_ORDER.length; i++) {
|
||||
const key = STACK_ORDER[i];
|
||||
const val = key ? (payload?.[key] ?? 0) : 0;
|
||||
if (val > 0) {
|
||||
hasAbove = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const radius = hasAbove ? [0, 0, 0, 0] : [2.5, 2.5, 0, 0];
|
||||
return <Rectangle {...props} radius={radius as any} />;
|
||||
};
|
||||
}
|
||||
|
||||
export default function EmailChart({ days, domain }: EmailChartProps) {
|
||||
const domainId = domain ? Number(domain) : undefined;
|
||||
const statusQuery = api.dashboard.emailTimeSeries.useQuery({
|
||||
@@ -197,15 +229,32 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
|
||||
dataKey="delivered"
|
||||
stackId="a"
|
||||
fill={currentColors.delivered}
|
||||
shape={createRoundedTopShape("delivered")}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="bounced"
|
||||
stackId="a"
|
||||
fill={currentColors.bounced}
|
||||
shape={createRoundedTopShape("bounced")}
|
||||
/>
|
||||
<Bar dataKey="bounced" stackId="a" fill={currentColors.bounced} />
|
||||
<Bar
|
||||
dataKey="complained"
|
||||
stackId="a"
|
||||
fill={currentColors.complained}
|
||||
shape={createRoundedTopShape("complained")}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="opened"
|
||||
stackId="a"
|
||||
fill={currentColors.opened}
|
||||
shape={createRoundedTopShape("opened")}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="clicked"
|
||||
stackId="a"
|
||||
fill={currentColors.clicked}
|
||||
shape={createRoundedTopShape("clicked")}
|
||||
/>
|
||||
<Bar dataKey="opened" stackId="a" fill={currentColors.opened} />
|
||||
<Bar dataKey="clicked" stackId="a" fill={currentColors.clicked} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
@@ -94,7 +94,7 @@ export function ReputationMetrics({ days, domain }: ReputationMetricsProps) {
|
||||
<div className="w-1/2 border rounded-xl shadow p-4">
|
||||
<div className="flex justify-between">
|
||||
<div className=" flex items-center gap-2">
|
||||
<div className="text-muted-foreground">Bounce Rate</div>
|
||||
<div className="text-muted-foreground font-mono">Bounce Rate</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className=" h-3.5 w-3.5 text-muted-foreground" />
|
||||
@@ -108,7 +108,7 @@ export function ReputationMetrics({ days, domain }: ReputationMetricsProps) {
|
||||
<div></div>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-4">
|
||||
<div className="text-2xl mt-2">
|
||||
<div className="text-2xl mt-2 font-mono">
|
||||
{metrics?.bounceRate.toFixed(2)}%
|
||||
</div>
|
||||
<StatusBadge status={bounceStatus} />
|
||||
@@ -242,7 +242,9 @@ export function ReputationMetrics({ days, domain }: ReputationMetricsProps) {
|
||||
</div>
|
||||
<div className="w-1/2 border rounded-xl shadow p-4">
|
||||
<div className=" flex items-center gap-2">
|
||||
<div className=" text-muted-foreground">Complaint Rate</div>
|
||||
<div className=" text-muted-foreground font-mono">
|
||||
Complaint Rate
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className=" h-3.5 w-3.5 text-muted-foreground" />
|
||||
@@ -254,7 +256,7 @@ export function ReputationMetrics({ days, domain }: ReputationMetricsProps) {
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-4">
|
||||
<div className="text-2xl mt-2">
|
||||
<div className="text-2xl mt-2 font-mono">
|
||||
{metrics?.complaintRate.toFixed(2)}%
|
||||
</div>
|
||||
<StatusBadge status={complaintStatus} />
|
||||
|
@@ -39,6 +39,7 @@ import { isSelfHosted } from "~/utils/common";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Badge } from "@usesend/ui/src/badge";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@usesend/ui/src/avatar";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
@@ -55,9 +55,9 @@ function getProviders() {
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
authorization: {
|
||||
params: {
|
||||
scope: 'read:user user:email'
|
||||
}
|
||||
}
|
||||
scope: "read:user user:email",
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@@ -107,6 +107,9 @@ const config = {
|
||||
gray: {
|
||||
DEFAULT: "hsl(var(--gray))",
|
||||
},
|
||||
"primary-light": {
|
||||
DEFAULT: "hsl(var(--primary-light))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
|
@@ -1,148 +0,0 @@
|
||||
const codeTheme: {
|
||||
[key: string]: React.CSSProperties;
|
||||
} = {
|
||||
hljs: {
|
||||
display: "block",
|
||||
overflowX: "auto",
|
||||
padding: "0.5em",
|
||||
background: "#070808",
|
||||
color: "#d6deeb",
|
||||
},
|
||||
"hljs-keyword": {
|
||||
color: "#c792ea",
|
||||
fontStyle: "italic",
|
||||
},
|
||||
"hljs-built_in": {
|
||||
color: "#addb67",
|
||||
fontStyle: "italic",
|
||||
},
|
||||
"hljs-type": {
|
||||
color: "#82aaff",
|
||||
},
|
||||
"hljs-literal": {
|
||||
color: "#ff5874",
|
||||
},
|
||||
"hljs-number": {
|
||||
color: "#F78C6C",
|
||||
},
|
||||
"hljs-regexp": {
|
||||
color: "#5ca7e4",
|
||||
},
|
||||
"hljs-string": {
|
||||
color: "#ecc48d",
|
||||
},
|
||||
"hljs-subst": {
|
||||
color: "#d3423e",
|
||||
},
|
||||
"hljs-symbol": {
|
||||
color: "#82aaff",
|
||||
},
|
||||
"hljs-class": {
|
||||
color: "#ffcb8b",
|
||||
},
|
||||
"hljs-function": {
|
||||
color: "#82AAFF",
|
||||
},
|
||||
"hljs-title": {
|
||||
color: "#DCDCAA",
|
||||
fontStyle: "italic",
|
||||
},
|
||||
"hljs-params": {
|
||||
color: "#7fdbca",
|
||||
},
|
||||
"hljs-comment": {
|
||||
color: "#637777",
|
||||
fontStyle: "italic",
|
||||
},
|
||||
"hljs-doctag": {
|
||||
color: "#7fdbca",
|
||||
},
|
||||
"hljs-meta": {
|
||||
color: "#82aaff",
|
||||
},
|
||||
"hljs-meta-keyword": {
|
||||
color: "#82aaff",
|
||||
},
|
||||
"hljs-meta-string": {
|
||||
color: "#ecc48d",
|
||||
},
|
||||
"hljs-section": {
|
||||
color: "#82b1ff",
|
||||
},
|
||||
"hljs-tag": {
|
||||
color: "#7fdbca",
|
||||
},
|
||||
"hljs-name": {
|
||||
color: "#7fdbca",
|
||||
},
|
||||
"hljs-builtin-name": {
|
||||
color: "#7fdbca",
|
||||
},
|
||||
"hljs-attr": {
|
||||
color: "#7fdbca",
|
||||
},
|
||||
"hljs-attribute": {
|
||||
color: "#80cbc4",
|
||||
},
|
||||
"hljs-variable": {
|
||||
color: "#addb67",
|
||||
},
|
||||
"hljs-bullet": {
|
||||
color: "#d9f5dd",
|
||||
},
|
||||
"hljs-code": {
|
||||
color: "#80CBC4",
|
||||
},
|
||||
"hljs-emphasis": {
|
||||
color: "#c792ea",
|
||||
fontStyle: "italic",
|
||||
},
|
||||
"hljs-strong": {
|
||||
color: "#addb67",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
"hljs-formula": {
|
||||
color: "#c792ea",
|
||||
},
|
||||
"hljs-link": {
|
||||
color: "#ff869a",
|
||||
},
|
||||
"hljs-quote": {
|
||||
color: "#697098",
|
||||
fontStyle: "italic",
|
||||
},
|
||||
"hljs-selector-tag": {
|
||||
color: "#ff6363",
|
||||
},
|
||||
"hljs-selector-id": {
|
||||
color: "#fad430",
|
||||
},
|
||||
"hljs-selector-class": {
|
||||
color: "#addb67",
|
||||
fontStyle: "italic",
|
||||
},
|
||||
"hljs-selector-attr": {
|
||||
color: "#c792ea",
|
||||
fontStyle: "italic",
|
||||
},
|
||||
"hljs-selector-pseudo": {
|
||||
color: "#c792ea",
|
||||
fontStyle: "italic",
|
||||
},
|
||||
"hljs-template-tag": {
|
||||
color: "#c792ea",
|
||||
},
|
||||
"hljs-template-variable": {
|
||||
color: "#addb67",
|
||||
},
|
||||
"hljs-addition": {
|
||||
color: "#addb67ff",
|
||||
fontStyle: "italic",
|
||||
},
|
||||
"hljs-deletion": {
|
||||
color: "#EF535090",
|
||||
fontStyle: "italic",
|
||||
},
|
||||
};
|
||||
|
||||
export default codeTheme;
|
@@ -16,7 +16,6 @@
|
||||
"@types/node": "^22.15.2",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@usesend/eslint-config": "workspace:*",
|
||||
"@usesend/tailwind-config": "workspace:*",
|
||||
"@usesend/typescript-config": "workspace:*",
|
||||
@@ -48,16 +47,17 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"framer-motion": "^12.9.2",
|
||||
"hast-util-to-jsx-runtime": "^2.3.6",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.503.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"pnpm": "^10.9.0",
|
||||
"react-hook-form": "^7.56.1",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"recharts": "^2.15.3",
|
||||
"shiki": "^3.3.0",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.24.3"
|
||||
}
|
||||
}
|
||||
}
|
36
packages/ui/src/code-block.tsx
Normal file
36
packages/ui/src/code-block.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { BundledLanguage, codeToHast } from "shiki";
|
||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime";
|
||||
import { Fragment } from "react";
|
||||
import { jsx, jsxs } from "react/jsx-runtime";
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
interface Props {
|
||||
children: string;
|
||||
lang: BundledLanguage;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export async function CodeBlock(props: Props) {
|
||||
const out = await codeToHast(props.children, {
|
||||
lang: props.lang,
|
||||
themes: {
|
||||
dark: "catppuccin-mocha",
|
||||
light: "catppuccin-latte",
|
||||
},
|
||||
});
|
||||
|
||||
return toJsxRuntime(out, {
|
||||
Fragment,
|
||||
jsx,
|
||||
jsxs,
|
||||
components: {
|
||||
// your custom `pre` element
|
||||
pre: (nodeProps) => (
|
||||
<pre
|
||||
{...nodeProps}
|
||||
className={cn(nodeProps.className, props.className)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}) as React.JSX.Element;
|
||||
}
|
@@ -1,101 +0,0 @@
|
||||
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import js from "react-syntax-highlighter/dist/esm/languages/hljs/javascript";
|
||||
import ruby from "react-syntax-highlighter/dist/esm/languages/hljs/ruby";
|
||||
import php from "react-syntax-highlighter/dist/esm/languages/hljs/php";
|
||||
import python from "react-syntax-highlighter/dist/esm/languages/hljs/python";
|
||||
// import { nightOwl } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
import codeTheme from "../code-theme";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
|
||||
import { Button } from "./button";
|
||||
import { ClipboardCopy, Check } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
export type Language = "js" | "ruby" | "php" | "python" | "curl";
|
||||
|
||||
export type CodeBlock = {
|
||||
language: Language;
|
||||
title?: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
type CodeProps = {
|
||||
codeBlocks: CodeBlock[];
|
||||
codeClassName?: string;
|
||||
};
|
||||
|
||||
SyntaxHighlighter.registerLanguage("js", js);
|
||||
SyntaxHighlighter.registerLanguage("ruby", ruby);
|
||||
SyntaxHighlighter.registerLanguage("php", php);
|
||||
SyntaxHighlighter.registerLanguage("python", python);
|
||||
|
||||
export const Code: React.FC<CodeProps> = ({ codeBlocks, codeClassName }) => {
|
||||
const [selectedTab, setSelectedTab] = useState(
|
||||
codeBlocks[0]?.language ?? "js"
|
||||
);
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const copyToClipboard = async (code: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000); // Reset the icon back to clipboard after 2 seconds
|
||||
} catch (err) {
|
||||
alert("Failed to copy code");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-xl bg-background border shadow-[#1e293b] shadow-lg">
|
||||
<Tabs
|
||||
defaultValue={codeBlocks[0]?.language}
|
||||
onValueChange={(val) => setSelectedTab(val as Language)}
|
||||
>
|
||||
<div className="flex justify-between items-center border-b py-1 px-2">
|
||||
<TabsList className="w-full rounded-none justify-start bg-transparent h-12">
|
||||
<div className="">
|
||||
{codeBlocks.map((block) => (
|
||||
<TabsTrigger
|
||||
key={block.language}
|
||||
value={block.language}
|
||||
className="data-[state=active]:bg-accent py-0.5 px-4 "
|
||||
>
|
||||
{block.title || block.language}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</div>
|
||||
</TabsList>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="icon"
|
||||
onClick={() =>
|
||||
copyToClipboard(
|
||||
codeBlocks.find((block) => block.language === selectedTab)
|
||||
?.code || ""
|
||||
)
|
||||
}
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<ClipboardCopy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{codeBlocks.map((block) => (
|
||||
<TabsContent
|
||||
key={block.language}
|
||||
value={block.language}
|
||||
className="mt-0"
|
||||
>
|
||||
<div className={cn("overflow-auto rounded-b-xl", codeClassName)}>
|
||||
<SyntaxHighlighter language={block.language} style={codeTheme}>
|
||||
{block.code}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -14,7 +14,7 @@ export const H1 = React.forwardRef<HTMLHeadingElement, TypographyProps>(
|
||||
ref={ref}
|
||||
className={cn(
|
||||
// font-mono, larger and a bit bolder
|
||||
" font-mono text-xl font-medium",
|
||||
" font-mono text-xl font-medium text-primary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
@@ -14,7 +14,7 @@
|
||||
--popover: 220 2% 96%;
|
||||
--popover-foreground: 234 16% 35%;
|
||||
|
||||
--primary: 200 65% 14%;
|
||||
--primary: 167 34% 20%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
@@ -63,6 +63,7 @@
|
||||
--purple: 266 85% 58%;
|
||||
--yellow: 35 77% 49%;
|
||||
--gray: 220 9% 46%;
|
||||
--primary-light: 183 74% 35%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -75,7 +76,7 @@
|
||||
--popover: 240 21% 15%;
|
||||
--popover-foreground: 226 64% 88%;
|
||||
|
||||
--primary: 220 23% 95%;
|
||||
--primary: 167 64% 94%;
|
||||
--primary-foreground: 240 23% 9%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
@@ -122,6 +123,7 @@
|
||||
--purple: 267 84% 81%;
|
||||
--yellow: 41 86% 83%;
|
||||
--gray: 218 11% 65%;
|
||||
--primary-light: 170 57% 73%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +139,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.shiki,
|
||||
.shiki span {
|
||||
color: var(--shiki-dark) !important;
|
||||
background-color: var(--shiki-dark-bg) !important;
|
||||
/* Optional, if you also want font styles */
|
||||
font-style: var(--shiki-dark-font-style) !important;
|
||||
font-weight: var(--shiki-dark-font-weight) !important;
|
||||
text-decoration: var(--shiki-dark-text-decoration) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* .app,
|
||||
::before,
|
||||
::after {
|
||||
|
188
pnpm-lock.yaml
generated
188
pnpm-lock.yaml
generated
@@ -283,7 +283,7 @@ importers:
|
||||
version: 10.0.4(react@19.1.0)
|
||||
usesend:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/sdk
|
||||
version: link:../..
|
||||
zod:
|
||||
specifier: ^3.24.3
|
||||
version: 3.24.3
|
||||
@@ -608,6 +608,9 @@ importers:
|
||||
framer-motion:
|
||||
specifier: ^12.9.2
|
||||
version: 12.9.2(react-dom@19.1.0)(react@19.1.0)
|
||||
hast-util-to-jsx-runtime:
|
||||
specifier: ^2.3.6
|
||||
version: 2.3.6
|
||||
input-otp:
|
||||
specifier: ^1.4.2
|
||||
version: 1.4.2(react-dom@19.1.0)(react@19.1.0)
|
||||
@@ -623,12 +626,12 @@ importers:
|
||||
react-hook-form:
|
||||
specifier: ^7.56.1
|
||||
version: 7.56.1(react@19.1.0)
|
||||
react-syntax-highlighter:
|
||||
specifier: ^15.6.1
|
||||
version: 15.6.1(react@19.1.0)
|
||||
recharts:
|
||||
specifier: ^2.15.3
|
||||
version: 2.15.3(react-dom@19.1.0)(react@19.1.0)
|
||||
shiki:
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0
|
||||
sonner:
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3(react-dom@19.1.0)(react@19.1.0)
|
||||
@@ -654,9 +657,6 @@ importers:
|
||||
'@types/react-dom':
|
||||
specifier: ^19.1.2
|
||||
version: 19.1.2(@types/react@19.1.2)
|
||||
'@types/react-syntax-highlighter':
|
||||
specifier: ^15.5.13
|
||||
version: 15.5.13
|
||||
'@usesend/eslint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../eslint-config
|
||||
@@ -7591,7 +7591,6 @@ packages:
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
dev: true
|
||||
|
||||
/@types/es-aggregate-error@1.0.6:
|
||||
resolution: {integrity: sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg==}
|
||||
@@ -7610,7 +7609,6 @@ packages:
|
||||
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
|
||||
dependencies:
|
||||
'@types/estree': 1.0.7
|
||||
dev: true
|
||||
|
||||
/@types/estree@1.0.7:
|
||||
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
|
||||
@@ -7619,6 +7617,7 @@ packages:
|
||||
resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==}
|
||||
dependencies:
|
||||
'@types/unist': 2.0.11
|
||||
dev: true
|
||||
|
||||
/@types/hast@3.0.4:
|
||||
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||
@@ -7682,7 +7681,6 @@ packages:
|
||||
|
||||
/@types/ms@2.1.0:
|
||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||
dev: true
|
||||
|
||||
/@types/nlcst@2.0.3:
|
||||
resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==}
|
||||
@@ -7722,12 +7720,6 @@ packages:
|
||||
dependencies:
|
||||
'@types/react': 19.1.2
|
||||
|
||||
/@types/react-syntax-highlighter@15.5.13:
|
||||
resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==}
|
||||
dependencies:
|
||||
'@types/react': 19.1.2
|
||||
dev: true
|
||||
|
||||
/@types/react@19.1.2:
|
||||
resolution: {integrity: sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==}
|
||||
dependencies:
|
||||
@@ -9023,28 +9015,14 @@ packages:
|
||||
/character-entities-html4@2.1.0:
|
||||
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
|
||||
|
||||
/character-entities-legacy@1.1.4:
|
||||
resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==}
|
||||
dev: false
|
||||
|
||||
/character-entities-legacy@3.0.0:
|
||||
resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
|
||||
|
||||
/character-entities@1.2.4:
|
||||
resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==}
|
||||
dev: false
|
||||
|
||||
/character-entities@2.0.2:
|
||||
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
|
||||
dev: true
|
||||
|
||||
/character-reference-invalid@1.1.4:
|
||||
resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==}
|
||||
dev: false
|
||||
|
||||
/character-reference-invalid@2.0.1:
|
||||
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
|
||||
dev: true
|
||||
|
||||
/chardet@0.7.0:
|
||||
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
|
||||
@@ -9232,10 +9210,6 @@ packages:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
/comma-separated-tokens@1.0.8:
|
||||
resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==}
|
||||
dev: false
|
||||
|
||||
/comma-separated-tokens@2.0.3:
|
||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||
|
||||
@@ -9555,7 +9529,6 @@ packages:
|
||||
resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==}
|
||||
dependencies:
|
||||
character-entities: 2.0.2
|
||||
dev: true
|
||||
|
||||
/decode-uri-component@0.4.1:
|
||||
resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==}
|
||||
@@ -10706,7 +10679,6 @@ packages:
|
||||
|
||||
/estree-util-is-identifier-name@3.0.0:
|
||||
resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
|
||||
dev: true
|
||||
|
||||
/estree-util-scope@1.0.0:
|
||||
resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==}
|
||||
@@ -10908,12 +10880,6 @@ packages:
|
||||
dependencies:
|
||||
reusify: 1.1.0
|
||||
|
||||
/fault@1.0.4:
|
||||
resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==}
|
||||
dependencies:
|
||||
format: 0.2.2
|
||||
dev: false
|
||||
|
||||
/fault@2.0.1:
|
||||
resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==}
|
||||
dependencies:
|
||||
@@ -11058,6 +11024,7 @@ packages:
|
||||
/format@0.2.2:
|
||||
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
||||
engines: {node: '>=0.4.x'}
|
||||
dev: true
|
||||
|
||||
/forwarded@0.2.0:
|
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
@@ -11524,10 +11491,6 @@ packages:
|
||||
hast-util-whitespace: 3.0.0
|
||||
unist-util-is: 6.0.0
|
||||
|
||||
/hast-util-parse-selector@2.2.5:
|
||||
resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==}
|
||||
dev: false
|
||||
|
||||
/hast-util-parse-selector@3.1.1:
|
||||
resolution: {integrity: sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==}
|
||||
dependencies:
|
||||
@@ -11627,7 +11590,6 @@ packages:
|
||||
vfile-message: 4.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/hast-util-to-mdast@10.1.2:
|
||||
resolution: {integrity: sha512-FiCRI7NmOvM4y+f5w32jPRzcxDIz+PUqDwEqn1A+1q2cdp3B8Gx7aVrXORdOKjMNDQsD1ogOr896+0jJHW1EFQ==}
|
||||
@@ -11667,16 +11629,6 @@ packages:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
/hastscript@6.0.0:
|
||||
resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==}
|
||||
dependencies:
|
||||
'@types/hast': 2.3.10
|
||||
comma-separated-tokens: 1.0.8
|
||||
hast-util-parse-selector: 2.2.5
|
||||
property-information: 5.6.0
|
||||
space-separated-tokens: 1.1.5
|
||||
dev: false
|
||||
|
||||
/hastscript@7.2.0:
|
||||
resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==}
|
||||
dependencies:
|
||||
@@ -11705,14 +11657,6 @@ packages:
|
||||
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
|
||||
dev: false
|
||||
|
||||
/highlight.js@10.7.3:
|
||||
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
|
||||
dev: false
|
||||
|
||||
/highlightjs-vue@1.0.0:
|
||||
resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==}
|
||||
dev: false
|
||||
|
||||
/hono@4.7.7:
|
||||
resolution: {integrity: sha512-2PCpQRbN87Crty8/L/7akZN3UyZIAopSoRxCwRbJgUuV1+MHNFHzYFxZTg4v/03cXUm+jce/qa2VSBZpKBm3Qw==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
@@ -11956,27 +11900,14 @@ packages:
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/is-alphabetical@1.0.4:
|
||||
resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==}
|
||||
dev: false
|
||||
|
||||
/is-alphabetical@2.0.1:
|
||||
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
|
||||
dev: true
|
||||
|
||||
/is-alphanumerical@1.0.4:
|
||||
resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==}
|
||||
dependencies:
|
||||
is-alphabetical: 1.0.4
|
||||
is-decimal: 1.0.4
|
||||
dev: false
|
||||
|
||||
/is-alphanumerical@2.0.1:
|
||||
resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
|
||||
dependencies:
|
||||
is-alphabetical: 2.0.1
|
||||
is-decimal: 2.0.1
|
||||
dev: true
|
||||
|
||||
/is-array-buffer@3.0.5:
|
||||
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
|
||||
@@ -12072,13 +12003,8 @@ packages:
|
||||
has-tostringtag: 1.0.2
|
||||
dev: true
|
||||
|
||||
/is-decimal@1.0.4:
|
||||
resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==}
|
||||
dev: false
|
||||
|
||||
/is-decimal@2.0.1:
|
||||
resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
|
||||
dev: true
|
||||
|
||||
/is-docker@2.2.1:
|
||||
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
|
||||
@@ -12121,13 +12047,8 @@ packages:
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
|
||||
/is-hexadecimal@1.0.4:
|
||||
resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==}
|
||||
dev: false
|
||||
|
||||
/is-hexadecimal@2.0.1:
|
||||
resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
|
||||
dev: true
|
||||
|
||||
/is-interactive@2.0.0:
|
||||
resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==}
|
||||
@@ -12682,7 +12603,6 @@ packages:
|
||||
|
||||
/longest-streak@3.1.0:
|
||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||
dev: true
|
||||
|
||||
/loose-envify@1.4.0:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
@@ -12695,13 +12615,6 @@ packages:
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/lowlight@1.20.0:
|
||||
resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==}
|
||||
dependencies:
|
||||
fault: 1.0.4
|
||||
highlight.js: 10.7.3
|
||||
dev: false
|
||||
|
||||
/lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
@@ -12830,7 +12743,6 @@ packages:
|
||||
unist-util-stringify-position: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/mdast-util-frontmatter@2.0.1:
|
||||
resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==}
|
||||
@@ -12939,7 +12851,6 @@ packages:
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/mdast-util-mdx-jsx@3.2.0:
|
||||
resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==}
|
||||
@@ -12958,7 +12869,6 @@ packages:
|
||||
vfile-message: 4.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/mdast-util-mdx@3.0.0:
|
||||
resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==}
|
||||
@@ -12983,14 +12893,12 @@ packages:
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/mdast-util-phrasing@4.1.0:
|
||||
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
unist-util-is: 6.0.0
|
||||
dev: true
|
||||
|
||||
/mdast-util-to-hast@13.2.0:
|
||||
resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==}
|
||||
@@ -13017,13 +12925,11 @@ packages:
|
||||
micromark-util-decode-string: 2.0.1
|
||||
unist-util-visit: 5.0.0
|
||||
zwitch: 2.0.4
|
||||
dev: true
|
||||
|
||||
/mdast-util-to-string@4.0.0:
|
||||
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
dev: true
|
||||
|
||||
/mdast@3.0.0:
|
||||
resolution: {integrity: sha512-xySmf8g4fPKMeC07jXGz971EkLbWAJ83s4US2Tj9lEdnZ142UP5grN73H1Xd3HzrdbU5o9GYYP/y8F9ZSwLE9g==}
|
||||
@@ -13071,7 +12977,6 @@ packages:
|
||||
micromark-util-subtokenize: 2.1.0
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
dev: true
|
||||
|
||||
/micromark-extension-frontmatter@2.0.0:
|
||||
resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==}
|
||||
@@ -13233,7 +13138,6 @@ packages:
|
||||
micromark-util-character: 2.1.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
dev: true
|
||||
|
||||
/micromark-factory-label@2.0.1:
|
||||
resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==}
|
||||
@@ -13242,7 +13146,6 @@ packages:
|
||||
micromark-util-character: 2.1.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
dev: true
|
||||
|
||||
/micromark-factory-mdx-expression@2.0.3:
|
||||
resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==}
|
||||
@@ -13263,7 +13166,6 @@ packages:
|
||||
dependencies:
|
||||
micromark-util-character: 2.1.1
|
||||
micromark-util-types: 2.0.2
|
||||
dev: true
|
||||
|
||||
/micromark-factory-title@2.0.1:
|
||||
resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==}
|
||||
@@ -13272,7 +13174,6 @@ packages:
|
||||
micromark-util-character: 2.1.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
dev: true
|
||||
|
||||
/micromark-factory-whitespace@2.0.1:
|
||||
resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==}
|
||||
@@ -13281,7 +13182,6 @@ packages:
|
||||
micromark-util-character: 2.1.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
dev: true
|
||||
|
||||
/micromark-util-character@2.1.1:
|
||||
resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==}
|
||||
@@ -13293,7 +13193,6 @@ packages:
|
||||
resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==}
|
||||
dependencies:
|
||||
micromark-util-symbol: 2.0.1
|
||||
dev: true
|
||||
|
||||
/micromark-util-classify-character@2.0.1:
|
||||
resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==}
|
||||
@@ -13301,20 +13200,17 @@ packages:
|
||||
micromark-util-character: 2.1.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
dev: true
|
||||
|
||||
/micromark-util-combine-extensions@2.0.1:
|
||||
resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==}
|
||||
dependencies:
|
||||
micromark-util-chunked: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
dev: true
|
||||
|
||||
/micromark-util-decode-numeric-character-reference@2.0.2:
|
||||
resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==}
|
||||
dependencies:
|
||||
micromark-util-symbol: 2.0.1
|
||||
dev: true
|
||||
|
||||
/micromark-util-decode-string@2.0.1:
|
||||
resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==}
|
||||
@@ -13323,7 +13219,6 @@ packages:
|
||||
micromark-util-character: 2.1.1
|
||||
micromark-util-decode-numeric-character-reference: 2.0.2
|
||||
micromark-util-symbol: 2.0.1
|
||||
dev: true
|
||||
|
||||
/micromark-util-encode@2.0.1:
|
||||
resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==}
|
||||
@@ -13342,19 +13237,16 @@ packages:
|
||||
|
||||
/micromark-util-html-tag-name@2.0.1:
|
||||
resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==}
|
||||
dev: true
|
||||
|
||||
/micromark-util-normalize-identifier@2.0.1:
|
||||
resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==}
|
||||
dependencies:
|
||||
micromark-util-symbol: 2.0.1
|
||||
dev: true
|
||||
|
||||
/micromark-util-resolve-all@2.0.1:
|
||||
resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==}
|
||||
dependencies:
|
||||
micromark-util-types: 2.0.2
|
||||
dev: true
|
||||
|
||||
/micromark-util-sanitize-uri@2.0.1:
|
||||
resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==}
|
||||
@@ -13370,7 +13262,6 @@ packages:
|
||||
micromark-util-chunked: 2.0.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
dev: true
|
||||
|
||||
/micromark-util-symbol@2.0.1:
|
||||
resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==}
|
||||
@@ -13400,7 +13291,6 @@ packages:
|
||||
micromark-util-types: 2.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/micromatch@4.0.8:
|
||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||
@@ -14180,17 +14070,6 @@ packages:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
|
||||
/parse-entities@2.0.0:
|
||||
resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==}
|
||||
dependencies:
|
||||
character-entities: 1.2.4
|
||||
character-entities-legacy: 1.1.4
|
||||
character-reference-invalid: 1.1.4
|
||||
is-alphanumerical: 1.0.4
|
||||
is-decimal: 1.0.4
|
||||
is-hexadecimal: 1.0.4
|
||||
dev: false
|
||||
|
||||
/parse-entities@4.0.2:
|
||||
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
|
||||
dependencies:
|
||||
@@ -14201,7 +14080,6 @@ packages:
|
||||
is-alphanumerical: 2.0.1
|
||||
is-decimal: 2.0.1
|
||||
is-hexadecimal: 2.0.1
|
||||
dev: true
|
||||
|
||||
/parse-json@5.2.0:
|
||||
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||
@@ -14634,16 +14512,6 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/prismjs@1.27.0:
|
||||
resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/prismjs@1.30.0:
|
||||
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/process-warning@5.0.0:
|
||||
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
|
||||
dev: false
|
||||
@@ -14660,12 +14528,6 @@ packages:
|
||||
object-assign: 4.1.1
|
||||
react-is: 16.13.1
|
||||
|
||||
/property-information@5.6.0:
|
||||
resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==}
|
||||
dependencies:
|
||||
xtend: 4.0.2
|
||||
dev: false
|
||||
|
||||
/property-information@6.5.0:
|
||||
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
|
||||
dev: true
|
||||
@@ -15125,20 +14987,6 @@ packages:
|
||||
tslib: 2.8.1
|
||||
dev: false
|
||||
|
||||
/react-syntax-highlighter@15.6.1(react@19.1.0):
|
||||
resolution: {integrity: sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==}
|
||||
peerDependencies:
|
||||
react: '>= 0.14.0'
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.0
|
||||
highlight.js: 10.7.3
|
||||
highlightjs-vue: 1.0.0
|
||||
lowlight: 1.20.0
|
||||
prismjs: 1.30.0
|
||||
react: 19.1.0
|
||||
refractor: 3.6.0
|
||||
dev: false
|
||||
|
||||
/react-transition-group@4.4.5(react-dom@19.1.0)(react@19.1.0):
|
||||
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
|
||||
peerDependencies:
|
||||
@@ -15302,14 +15150,6 @@ packages:
|
||||
which-builtin-type: 1.2.1
|
||||
dev: true
|
||||
|
||||
/refractor@3.6.0:
|
||||
resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==}
|
||||
dependencies:
|
||||
hastscript: 6.0.0
|
||||
parse-entities: 2.0.0
|
||||
prismjs: 1.27.0
|
||||
dev: false
|
||||
|
||||
/refractor@4.9.0:
|
||||
resolution: {integrity: sha512-nEG1SPXFoGGx+dcjftjv8cAjEusIh6ED1xhf5DG3C0x/k+rmZ2duKnc3QLpt6qeHv5fPb8uwN3VWN2BT7fr3Og==}
|
||||
dependencies:
|
||||
@@ -16300,10 +16140,6 @@ packages:
|
||||
whatwg-url: 7.1.0
|
||||
dev: true
|
||||
|
||||
/space-separated-tokens@1.1.5:
|
||||
resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==}
|
||||
dev: false
|
||||
|
||||
/space-separated-tokens@2.0.2:
|
||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||
|
||||
@@ -16533,7 +16369,6 @@ packages:
|
||||
resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==}
|
||||
dependencies:
|
||||
style-to-object: 1.0.8
|
||||
dev: true
|
||||
|
||||
/style-to-object@1.0.8:
|
||||
resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==}
|
||||
@@ -17596,11 +17431,6 @@ packages:
|
||||
engines: {node: '>=4.0'}
|
||||
dev: true
|
||||
|
||||
/xtend@4.0.2:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
dev: false
|
||||
|
||||
/y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
|
Reference in New Issue
Block a user