Initial push of Plunk Next
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import {WIKI_URI} from '../../lib/constants';
|
||||
import logo from '../../../public/assets/logo.svg';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default function Footer() {
|
||||
return (
|
||||
<>
|
||||
<footer className={'border-t border-neutral-200 bg-white'}>
|
||||
<div className="mx-auto max-w-7xl px-8 py-20 xl:px-0">
|
||||
<div className="grid gap-12 lg:grid-cols-12">
|
||||
{/* Logo and description */}
|
||||
<div className="space-y-6 lg:col-span-4">
|
||||
<div className={'relative h-8 w-8'}>
|
||||
<Image src={logo} alt={'Plunk logo'} fill className={'object-contain'} />
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed text-neutral-600">
|
||||
Open-source email automation platform that scales
|
||||
</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href="https://twitter.com/useplunk"
|
||||
target={'_blank'}
|
||||
className="text-neutral-500 transition hover:text-neutral-900"
|
||||
>
|
||||
<span className="sr-only">Twitter</span>
|
||||
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84" />
|
||||
</svg>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="https://github.com/useplunk"
|
||||
target={'_blank'}
|
||||
className="text-neutral-500 transition hover:text-neutral-900"
|
||||
>
|
||||
<span className="sr-only">GitHub</span>
|
||||
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
|
||||
<Link href="/discord" target={'_blank'} className="text-neutral-500 transition hover:text-neutral-900">
|
||||
<span className="sr-only">Discord</span>
|
||||
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Links */}
|
||||
<div className="grid grid-cols-2 gap-8 lg:col-span-8 lg:grid-cols-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-neutral-900">Product</h3>
|
||||
<ul role="list" className="mt-6 space-y-4">
|
||||
<li>
|
||||
<Link href={'/pricing'} className="text-sm text-neutral-600 transition hover:text-neutral-900">
|
||||
Pricing
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href={'/made-by-humans'} className="text-sm text-neutral-600 transition hover:text-neutral-900">
|
||||
Made by humans
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href={WIKI_URI}
|
||||
target={'_blank'}
|
||||
className="text-sm text-neutral-600 transition hover:text-neutral-900"
|
||||
>
|
||||
Documentation
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-neutral-900">Community</h3>
|
||||
<ul role="list" className="mt-6 space-y-4">
|
||||
<li>
|
||||
<Link href={'/discord'} className="text-sm text-neutral-600 transition hover:text-neutral-900">
|
||||
Discord
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href={'https://github.com/useplunk'}
|
||||
target={'_blank'}
|
||||
className="text-sm text-neutral-600 transition hover:text-neutral-900"
|
||||
>
|
||||
GitHub
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href={'https://status.useplunk.com'}
|
||||
target={'_blank'}
|
||||
className="text-sm text-neutral-600 transition hover:text-neutral-900"
|
||||
>
|
||||
Status
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-neutral-900">Legal</h3>
|
||||
<ul role="list" className="mt-6 space-y-4">
|
||||
<li>
|
||||
<Link href={'/privacy'} className="text-sm text-neutral-600 transition hover:text-neutral-900">
|
||||
Privacy
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href={'/terms'} className="text-sm text-neutral-600 transition hover:text-neutral-900">
|
||||
Terms
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-16 border-t border-neutral-200 pt-8">
|
||||
<p className="text-sm text-neutral-500">
|
||||
© {new Date().getFullYear()} Plunk. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {default as Footer} from './Footer';
|
||||
@@ -0,0 +1,178 @@
|
||||
import Link from 'next/link';
|
||||
import React, {useState} from 'react';
|
||||
import {AnimatePresence, motion} from 'framer-motion';
|
||||
import {DASHBOARD_URI, WIKI_URI} from '../../lib/constants';
|
||||
import Image from 'next/image';
|
||||
import logo from '../../../public/assets/logo.svg';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default function Navbar() {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<nav className={'top-0 z-40 mx-auto max-w-7xl px-8 xl:px-0'}>
|
||||
<div className={'z-40 py-6'}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-12">
|
||||
<div className="flex flex-shrink-0 items-center">
|
||||
<Link href={'/'} className={'flex items-center gap-x-3'}>
|
||||
<div className={'relative h-8 w-8'}>
|
||||
<Image src={logo} alt={'Plunk logo'} fill className={'object-contain'} />
|
||||
</div>
|
||||
<span className={'sr-only'}>Plunk</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="hidden items-center gap-8 md:flex">
|
||||
<Link href={'/made-by-humans'} className={'text-sm font-medium text-neutral-600 transition hover:text-neutral-900'}>
|
||||
By humans
|
||||
</Link>
|
||||
|
||||
<Link href={'/pricing'} className={'text-sm font-medium text-neutral-600 transition hover:text-neutral-900'}>
|
||||
Pricing
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href={WIKI_URI}
|
||||
target={'_blank'}
|
||||
rel={'noreferrer'}
|
||||
className={'flex items-center gap-x-1.5 text-sm font-medium text-neutral-600 transition hover:text-neutral-900'}
|
||||
>
|
||||
Docs
|
||||
<svg className={'h-3.5 w-3.5'} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9.25 4.75H6.75C5.64543 4.75 4.75 5.64543 4.75 6.75V17.25C4.75 18.3546 5.64543 19.25 6.75 19.25H17.25C18.3546 19.25 19.25 18.3546 19.25 17.25V14.75"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M19.25 9.25V4.75H14.75"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M19 5L11.75 12.25"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center gap-6 md:flex">
|
||||
<a href={`${DASHBOARD_URI}/auth/login`} className={'text-sm font-medium text-neutral-600 transition hover:text-neutral-900'}>
|
||||
Sign in
|
||||
</a>
|
||||
<motion.a
|
||||
whileHover={{scale: 1.02}}
|
||||
whileTap={{scale: 0.98}}
|
||||
href={`${DASHBOARD_URI}/auth/signup`}
|
||||
className={'rounded-lg bg-neutral-900 px-6 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-neutral-800'}
|
||||
>
|
||||
Get started
|
||||
</motion.a>
|
||||
</div>
|
||||
|
||||
<div className="-mr-2 flex items-center md:hidden">
|
||||
<button
|
||||
onClick={() => setMobileOpen(!mobileOpen)}
|
||||
type="button"
|
||||
className="inline-flex items-center justify-center rounded-lg p-2 text-neutral-600 hover:bg-neutral-100 hover:text-neutral-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-neutral-900"
|
||||
aria-controls="mobile-menu"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span className="sr-only">Open main menu</span>
|
||||
|
||||
<svg
|
||||
className={`${mobileOpen ? 'hidden' : 'block'} h-6 w-6`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
className={`${!mobileOpen ? 'hidden' : 'block'} h-6 w-6`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{mobileOpen && (
|
||||
<motion.div
|
||||
initial={{height: 0, opacity: 0}}
|
||||
animate={{height: 'auto', opacity: 1}}
|
||||
exit={{height: 0, opacity: 0}}
|
||||
transition={{duration: 0.2}}
|
||||
className="absolute left-0 top-full z-50 mt-2 w-full rounded-lg border border-neutral-200 bg-white shadow-lg backdrop-blur-sm sm:hidden"
|
||||
>
|
||||
<motion.div
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
exit={{opacity: 0}}
|
||||
transition={{duration: 0.2}}
|
||||
className="space-y-1 p-4"
|
||||
>
|
||||
<Link
|
||||
href={'/made-by-humans'}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="block rounded-lg px-4 py-3 text-sm font-medium text-neutral-600 transition hover:bg-neutral-100 hover:text-neutral-900"
|
||||
>
|
||||
By humans
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href={'/pricing'}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="block rounded-lg px-4 py-3 text-sm font-medium text-neutral-600 transition hover:bg-neutral-100 hover:text-neutral-900"
|
||||
>
|
||||
Pricing
|
||||
</Link>
|
||||
|
||||
<a
|
||||
href={WIKI_URI}
|
||||
target={'_blank'}
|
||||
rel={'noreferrer'}
|
||||
className="block rounded-lg px-4 py-3 text-sm font-medium text-neutral-600 transition hover:bg-neutral-100 hover:text-neutral-900"
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
|
||||
<div className="border-t border-neutral-200 pt-4">
|
||||
<a
|
||||
href={`${DASHBOARD_URI}/auth/login`}
|
||||
className="block rounded-lg px-4 py-3 text-sm font-medium text-neutral-600 transition hover:bg-neutral-100 hover:text-neutral-900"
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
<a
|
||||
href={`${DASHBOARD_URI}/auth/signup`}
|
||||
className="mt-2 block rounded-lg bg-neutral-900 px-4 py-3 text-center text-sm font-semibold text-white transition hover:bg-neutral-800"
|
||||
>
|
||||
Get started
|
||||
</a>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {default as Navbar} from './Navbar';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Navbar';
|
||||
export * from './Footer';
|
||||
@@ -0,0 +1,8 @@
|
||||
// Environment configuration
|
||||
// In development: Uses NEXT_PUBLIC_* env vars from .env file
|
||||
// In production (Docker): Build-time values are replaced at container startup via sed
|
||||
|
||||
export const API_URI = process.env.NEXT_PUBLIC_API_URI || 'http://localhost:8080';
|
||||
export const DASHBOARD_URI = process.env.NEXT_PUBLIC_DASHBOARD_URI || 'http://localhost:3000';
|
||||
export const LANDING_URI = process.env.NEXT_PUBLIC_LANDING_URI || 'http://localhost:4000';
|
||||
export const WIKI_URI = process.env.NEXT_PUBLIC_WIKI_URI || 'http://localhost:1000';
|
||||
@@ -0,0 +1,10 @@
|
||||
import {User} from '@prisma/client';
|
||||
import useSWR from 'swr';
|
||||
|
||||
/**
|
||||
* Fetch the current account. undefined means loading, null means logged out
|
||||
*
|
||||
*/
|
||||
export function useAccount() {
|
||||
return useSWR<User | null>('/users/@me', {shouldRetryOnError: false});
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import useSWR from 'swr';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function useStats() {
|
||||
return useSWR<{
|
||||
emails: number;
|
||||
events: number;
|
||||
projects: number;
|
||||
contacts: number;
|
||||
}>('/utils', {
|
||||
shouldRetryOnError: false,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {useEffect, useLayoutEffect, useRef} from 'react';
|
||||
|
||||
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param callback
|
||||
* @param delay
|
||||
*/
|
||||
function useInterval(callback: () => void, delay: number | null) {
|
||||
const savedCallback = useRef(callback);
|
||||
|
||||
// Remember the latest callback if it changes.
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
// Set up the interval.
|
||||
useEffect(() => {
|
||||
// Don't schedule if no delay is specified.
|
||||
// Note: 0 is a valid value for delay.
|
||||
if (!delay && delay !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = setInterval(() => savedCallback.current(), delay);
|
||||
|
||||
return () => clearInterval(id);
|
||||
}, [delay]);
|
||||
}
|
||||
|
||||
export default useInterval;
|
||||
@@ -0,0 +1,43 @@
|
||||
import {API_URI} from './constants';
|
||||
import {infer as ZodInfer, ZodSchema} from 'zod';
|
||||
|
||||
interface Json {
|
||||
[x: string]: string | number | boolean | Date | Json | JsonArray;
|
||||
}
|
||||
|
||||
type JsonArray = (string | number | boolean | Date | Json | JsonArray)[];
|
||||
|
||||
interface TypedSchema extends ZodSchema {
|
||||
_type: unknown;
|
||||
}
|
||||
|
||||
export class network {
|
||||
/**
|
||||
* Fetcher function that includes toast support
|
||||
* @param method Request method
|
||||
* @param path Request endpoint or path
|
||||
* @param body Request body
|
||||
*/
|
||||
public static async fetch<T, Schema extends TypedSchema | void = void>(
|
||||
method: 'GET' | 'PUT' | 'POST' | 'DELETE',
|
||||
path: string,
|
||||
body?: Schema extends TypedSchema ? ZodInfer<Schema> : never,
|
||||
): Promise<T> {
|
||||
const url = path.startsWith('http') ? path : API_URI + path;
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
body: body && JSON.stringify(body),
|
||||
headers: body && {'Content-Type': 'application/json'},
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
const res = await response.json();
|
||||
|
||||
if (response.status >= 400) {
|
||||
|
||||
throw new Error(res?.message ?? 'Something went wrong!');
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import '../styles/globals.css';
|
||||
import React, {useEffect} from 'react';
|
||||
import Head from 'next/head';
|
||||
import {AppProps} from 'next/app';
|
||||
import {toast, Toaster} from 'sonner';
|
||||
import {SWRConfig} from 'swr';
|
||||
import {network} from '../lib/network';
|
||||
import {DefaultSeo} from 'next-seo';
|
||||
|
||||
/**
|
||||
* Main app component
|
||||
* @param props Props
|
||||
* @param props.Component App component
|
||||
* @param props.pageProps
|
||||
*/
|
||||
function App({Component, pageProps}: AppProps) {
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const message = searchParams.get('message');
|
||||
|
||||
if (message) {
|
||||
toast(message, {duration: 10000});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Plunk | The Open-Source Email Platform</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" key={'viewport'} />
|
||||
</Head>
|
||||
<Toaster position={'top-right'} />
|
||||
|
||||
<Component {...pageProps} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main app root component that houses all components
|
||||
* @param props Default nextjs props
|
||||
*/
|
||||
export default function WithProviders(props: AppProps) {
|
||||
return (
|
||||
<SWRConfig
|
||||
value={{
|
||||
fetcher: (url: string) => network.fetch('GET', url),
|
||||
revalidateOnFocus: true,
|
||||
}}
|
||||
>
|
||||
<DefaultSeo
|
||||
defaultTitle={'Plunk | The Open-Source Email Platform'}
|
||||
title={'Plunk | The Open-Source Email Platform'}
|
||||
description={
|
||||
'Open-source email automation platform with workflows, segments, and developer API. Scale from 0 to millions of emails at $0.001 per email. Self-hostable and privacy-first.'
|
||||
}
|
||||
twitter={{cardType: 'summary_large_image', handle: '@useplunk', site: '@useplunk'}}
|
||||
openGraph={{
|
||||
title: 'Plunk | The Open-Source Email Platform',
|
||||
description:
|
||||
'Open-source email automation platform with workflows, segments, and developer API. Scale from 0 to millions of emails at $0.001 per email. Self-hostable and privacy-first.',
|
||||
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk'}],
|
||||
}}
|
||||
additionalMetaTags={[{property: 'title', content: 'Plunk | The Open-Source Email Platform'}]}
|
||||
/>
|
||||
|
||||
<App {...props} />
|
||||
</SWRConfig>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import Document, {Head, Html, Main, NextScript} from 'next/document';
|
||||
import React from 'react';
|
||||
|
||||
export default class MyDocument extends Document {
|
||||
public render() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
{/* Start fonts */}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
{/* End fonts */}
|
||||
{/* Start favicon */}
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/favicon/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/favicon/safari-pinned-tab.svg" color="#171717" />
|
||||
<link rel="shortcut icon" href="/favicon/favicon.ico" />
|
||||
<meta name="msapplication-TileColor" content="#ffffff" />
|
||||
<meta name="msapplication-config" content="/favicon/browserconfig.xml" />
|
||||
<meta name="theme-color" content="#171717" />
|
||||
{/* End favicon */}
|
||||
</Head>
|
||||
<body className={'cursor-default antialiased'}>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import {useRouter} from 'next/router';
|
||||
import {useEffect} from 'react';
|
||||
|
||||
/**
|
||||
* Instant redirect to the Discord server.
|
||||
*/
|
||||
export default function Index() {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
void router.push('https://discord.gg/sAPURyakc4');
|
||||
}, [router]);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
import {Footer, Navbar} from '../components';
|
||||
import {motion} from 'framer-motion';
|
||||
import {DASHBOARD_URI, LANDING_URI, WIKI_URI} from '../lib/constants';
|
||||
import React from 'react';
|
||||
import Artur from '../../public/assets/artur.png';
|
||||
import Joe from '../../public/assets/joe.png';
|
||||
import Noah from '../../public/assets/noah.png';
|
||||
import Pierre from '../../public/assets/pierre.png';
|
||||
import Jonni from '../../public/assets/jonni.png';
|
||||
import Alisson from '../../public/assets/alisson.png';
|
||||
import Image from 'next/image';
|
||||
import Script from 'next/script';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
Workflow,
|
||||
Users,
|
||||
Mail,
|
||||
BarChart3,
|
||||
Code2,
|
||||
Globe,
|
||||
Shield,
|
||||
PackageOpen,
|
||||
ArrowRight,
|
||||
Clock,
|
||||
Lock,
|
||||
TrendingUp,
|
||||
User,
|
||||
Megaphone,
|
||||
Send,
|
||||
} from 'lucide-react';
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
testimonial: 'Transparent and intuitive UI, extremely easy setup & automation and great support.',
|
||||
author: 'Artur Czemiel',
|
||||
image: Artur,
|
||||
role: 'Founder at GraphQL Editor',
|
||||
},
|
||||
{
|
||||
testimonial: 'Lots of care put into Plunk',
|
||||
author: 'Jonni Lundy',
|
||||
image: Jonni,
|
||||
role: 'Founding Operations Manager at Resend',
|
||||
},
|
||||
{
|
||||
testimonial: "I've been using Plunk for building & sending out marketing emails and genuinely love it!",
|
||||
author: 'Joe Ashwell',
|
||||
image: Joe,
|
||||
role: 'Founder at UnwindHR',
|
||||
},
|
||||
{
|
||||
testimonial: 'I loved the ease of use, beautiful UI and great UX. Everything simply works.',
|
||||
author: 'Alisson Leal',
|
||||
image: Alisson,
|
||||
role: 'Founder at Brapi',
|
||||
},
|
||||
{
|
||||
testimonial: 'Simple to use, efficient and no regrets!',
|
||||
author: 'Noah Di Gesu',
|
||||
image: Noah,
|
||||
role: 'Founder at Smoothey',
|
||||
},
|
||||
{
|
||||
testimonial: 'Clean design, easy to understand, fair pricing.',
|
||||
author: 'Pierre Jacquel',
|
||||
image: Pierre,
|
||||
role: 'Founder at Landingly',
|
||||
},
|
||||
];
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <Workflow className="h-5 w-5" />,
|
||||
title: 'Workflow Automation',
|
||||
description: 'Visual builder for complex email sequences with triggers, delays, and conditional logic.',
|
||||
},
|
||||
{
|
||||
icon: <Users className="h-5 w-5" />,
|
||||
title: 'Dynamic Segments',
|
||||
description: 'Real-time audience segmentation based on contact data, behavior, and custom fields.',
|
||||
},
|
||||
{
|
||||
icon: <Mail className="h-5 w-5" />,
|
||||
title: 'Campaign Management',
|
||||
description: 'Broadcast emails with scheduling, A/B testing, and performance tracking.',
|
||||
},
|
||||
{
|
||||
icon: <BarChart3 className="h-5 w-5" />,
|
||||
title: 'Analytics',
|
||||
description: 'Detailed metrics on opens, clicks, bounces, and conversions across campaigns.',
|
||||
},
|
||||
{
|
||||
icon: <Code2 className="h-5 w-5" />,
|
||||
title: 'Developer API',
|
||||
description: 'RESTful API with comprehensive documentation and language-specific SDKs.',
|
||||
},
|
||||
{
|
||||
icon: <Globe className="h-5 w-5" />,
|
||||
title: 'Custom Domains',
|
||||
description: 'Brand consistency with DKIM authentication and custom sending domains.',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default function Index() {
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
id={`faq-schema-index`}
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
'mainEntity': [
|
||||
{
|
||||
'@type': 'Question',
|
||||
'name': 'How is Plunk different from other email automation tools?',
|
||||
'acceptedAnswer': {
|
||||
'@type': 'Answer',
|
||||
'text':
|
||||
'Plunk is the email tool built for SaaS businesses, indie hackers and developers. Plunk allows you to create complex, automated email flows and to trigger them from anywhere through a single API call.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
'name': 'Can I use Plunk for transactional emails?',
|
||||
'acceptedAnswer': {
|
||||
'@type': 'Answer',
|
||||
'text':
|
||||
'Yes, Plunk is a full-fledged email tool that allows you to send transactional emails as well as marketing emails. You can use Plunk to send emails to your customers when they sign up, when they cancel their subscription, when they upgrade their plan, etc.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
'name': 'Can I use Plunk for sending newsletters?',
|
||||
'acceptedAnswer': {
|
||||
'@type': 'Answer',
|
||||
'text':
|
||||
'Yes, Plunk allows you to send newsletters and other broadcast emails to multiple contacts at once.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
'name': 'What programming languages and frameworks does Plunk support?',
|
||||
'acceptedAnswer': {
|
||||
'@type': 'Answer',
|
||||
'text':
|
||||
'Plunk supports all programming languages and frameworks that are capable of sending API calls.. You can use Plunk to send emails from your backend, frontend, mobile app, or any other platform.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
'name': 'How much does Plunk cost?',
|
||||
'acceptedAnswer': {
|
||||
'@type': 'Answer',
|
||||
'text':
|
||||
'Plunk offers a free plan that includes 3000 automated, 3000 transactional and 3000 broadcast emails per month. You can upgrade to a pay-as-you-go plan that costs $0.001 per email.',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
<Script
|
||||
id={`corp-schema-index`}
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Corporation',
|
||||
'name': 'Plunk',
|
||||
'alternateName': 'UsePlunk',
|
||||
'url': LANDING_URI,
|
||||
'logo': `${LANDING_URI}/assets/logo.png`,
|
||||
'sameAs': ['https://www.twitter.com/useplunk', LANDING_URI],
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Navbar />
|
||||
|
||||
<main className={'mx-auto max-w-7xl px-8 sm:px-0'}>
|
||||
{/* Hero Section */}
|
||||
<section className={'relative py-32 sm:py-48'}>
|
||||
{/* Subtle background grid */}
|
||||
<div
|
||||
className={
|
||||
'absolute inset-0 -z-10 h-full w-full bg-white bg-[linear-gradient(to_right,#e5e7eb_1px,transparent_1px),linear-gradient(to_bottom,#e5e7eb_1px,transparent_1px)] bg-[size:4rem_4rem] [mask-image:radial-gradient(ellipse_80%_50%_at_50%_0%,#000_70%,transparent_110%)]'
|
||||
}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.7, ease: [0.22, 1, 0.36, 1]}}
|
||||
className={'mx-auto max-w-4xl text-center'}
|
||||
>
|
||||
<h1 className={'text-7xl font-bold tracking-tight text-neutral-900 sm:text-8xl lg:text-9xl'}>
|
||||
Open-source
|
||||
<br />
|
||||
email platform
|
||||
</h1>
|
||||
<p className={'mx-auto mt-8 max-w-2xl text-xl text-neutral-600'}>
|
||||
Open-source email automation. Build workflows, segment audiences, and send emails with a simple API.
|
||||
</p>
|
||||
|
||||
<div className={'mt-12 flex flex-wrap justify-center gap-4'}>
|
||||
<motion.a
|
||||
whileHover={{scale: 1.02}}
|
||||
whileTap={{scale: 0.98}}
|
||||
href={`${DASHBOARD_URI}/auth/signup`}
|
||||
className={
|
||||
'group rounded-lg bg-neutral-900 px-8 py-4 text-base font-semibold text-white shadow-lg shadow-neutral-900/10 transition hover:bg-neutral-800'
|
||||
}
|
||||
>
|
||||
<span className={'flex items-center gap-2'}>
|
||||
Get started
|
||||
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
|
||||
</span>
|
||||
</motion.a>
|
||||
<Link
|
||||
href={WIKI_URI}
|
||||
target={'_blank'}
|
||||
className={
|
||||
'rounded-lg border border-neutral-300 bg-white px-8 py-4 text-base font-semibold text-neutral-900 transition hover:border-neutral-400'
|
||||
}
|
||||
>
|
||||
Documentation
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
|
||||
{/* Problem Statement */}
|
||||
<section className={'py-32'}>
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.7, ease: [0.22, 1, 0.36, 1]}}
|
||||
className={'mb-20 text-center'}
|
||||
>
|
||||
<h2 className={'text-5xl font-bold tracking-tight text-neutral-900'}>
|
||||
Most email tools weren't built to scale
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<div className={'grid gap-px bg-neutral-200 sm:grid-cols-3'}>
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.1, ease: [0.22, 1, 0.36, 1]}}
|
||||
className={'group bg-white p-12 transition hover:bg-neutral-50'}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'flex h-12 w-12 items-center justify-center rounded-xl bg-neutral-900 text-white transition group-hover:scale-110'
|
||||
}
|
||||
>
|
||||
<Clock className="h-5 w-5" />
|
||||
</div>
|
||||
<h3 className={'mt-6 text-lg font-semibold text-neutral-900'}>Complex setup</h3>
|
||||
<p className={'mt-2 text-sm leading-relaxed text-neutral-600'}>Hours of configuration needed</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.2, ease: [0.22, 1, 0.36, 1]}}
|
||||
className={'group bg-white p-12 transition hover:bg-neutral-50'}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'flex h-12 w-12 items-center justify-center rounded-xl bg-neutral-900 text-white transition group-hover:scale-110'
|
||||
}
|
||||
>
|
||||
<TrendingUp className="h-5 w-5" />
|
||||
</div>
|
||||
<h3 className={'mt-6 text-lg font-semibold text-neutral-900'}>Contact limits</h3>
|
||||
<p className={'mt-2 text-sm leading-relaxed text-neutral-600'}>Growth penalties</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.3, ease: [0.22, 1, 0.36, 1]}}
|
||||
className={'group bg-white p-12 transition hover:bg-neutral-50'}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'flex h-12 w-12 items-center justify-center rounded-xl bg-neutral-900 text-white transition group-hover:scale-110'
|
||||
}
|
||||
>
|
||||
<Lock className="h-5 w-5" />
|
||||
</div>
|
||||
<h3 className={'mt-6 text-lg font-semibold text-neutral-900'}>Vendor lock-in</h3>
|
||||
<p className={'mt-2 text-sm leading-relaxed text-neutral-600'}>Closed source platforms</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features */}
|
||||
<section className={'py-32'}>
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.7, ease: [0.22, 1, 0.36, 1]}}
|
||||
className={'mb-20 text-center'}
|
||||
>
|
||||
<h2 className={'text-5xl font-bold tracking-tight text-neutral-900'}>Built for scale</h2>
|
||||
<p className={'mt-4 text-lg text-neutral-600'}>Everything you need to run email at any volume</p>
|
||||
</motion.div>
|
||||
|
||||
<div className={'grid gap-px bg-neutral-200 sm:grid-cols-2 lg:grid-cols-3'}>
|
||||
{features.map((feature, index) => (
|
||||
<motion.div
|
||||
key={feature.title}
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: index * 0.1, ease: [0.22, 1, 0.36, 1]}}
|
||||
className={'group bg-white p-12 transition hover:bg-neutral-50'}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'flex h-12 w-12 items-center justify-center rounded-xl bg-neutral-900 text-white transition group-hover:scale-110'
|
||||
}
|
||||
>
|
||||
{feature.icon}
|
||||
</div>
|
||||
<h3 className={'mt-6 text-lg font-semibold text-neutral-900'}>{feature.title}</h3>
|
||||
<p className={'mt-2 text-sm leading-relaxed text-neutral-600'}>{feature.description}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Unified Contacts */}
|
||||
<section className={'py-32'}>
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.7, ease: [0.22, 1, 0.36, 1]}}
|
||||
className={'mx-auto max-w-4xl text-center'}
|
||||
>
|
||||
<h2 className={'text-5xl font-bold tracking-tight text-neutral-900'}>
|
||||
One contact, unified across everything
|
||||
</h2>
|
||||
<p className={'mt-6 text-lg text-neutral-600'}>
|
||||
Every interaction flows into a single contact record with complete history
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className={'mx-auto mt-20 max-w-5xl'}>
|
||||
{/* Email types */}
|
||||
<div className={'grid gap-8 lg:grid-cols-3'}>
|
||||
{/* Transactional */}
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.1, ease: [0.22, 1, 0.36, 1]}}
|
||||
className={'rounded-xl border border-neutral-200 bg-white p-8 text-center'}
|
||||
>
|
||||
<div
|
||||
className={'mx-auto flex h-16 w-16 items-center justify-center rounded-xl bg-neutral-900 text-white'}
|
||||
>
|
||||
<Send className="h-8 w-8" />
|
||||
</div>
|
||||
<h3 className={'mt-4 font-semibold text-neutral-900'}>Transactional</h3>
|
||||
<p className={'mt-2 text-sm text-neutral-600'}>Receipts, password resets</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Campaigns */}
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.2, ease: [0.22, 1, 0.36, 1]}}
|
||||
className={'rounded-xl border border-neutral-200 bg-white p-8 text-center'}
|
||||
>
|
||||
<div
|
||||
className={'mx-auto flex h-16 w-16 items-center justify-center rounded-xl bg-neutral-900 text-white'}
|
||||
>
|
||||
<Megaphone className="h-8 w-8" />
|
||||
</div>
|
||||
<h3 className={'mt-4 font-semibold text-neutral-900'}>Campaigns</h3>
|
||||
<p className={'mt-2 text-sm text-neutral-600'}>Newsletters, announcements</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Workflows */}
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.3, ease: [0.22, 1, 0.36, 1]}}
|
||||
className={'rounded-xl border border-neutral-200 bg-white p-8 text-center'}
|
||||
>
|
||||
<div
|
||||
className={'mx-auto flex h-16 w-16 items-center justify-center rounded-xl bg-neutral-900 text-white'}
|
||||
>
|
||||
<Workflow className="h-8 w-8" />
|
||||
</div>
|
||||
<h3 className={'mt-4 font-semibold text-neutral-900'}>Workflows</h3>
|
||||
<p className={'mt-2 text-sm text-neutral-600'}>Onboarding, drip sequences</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Arrows */}
|
||||
<div className={'relative my-12'}>
|
||||
<svg className={'mx-auto h-32 w-full'} viewBox="0 0 600 120" preserveAspectRatio="xMidYMid meet">
|
||||
<defs>
|
||||
<marker id="arrowhead" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
||||
<polygon points="0 0, 10 3, 0 6" fill="#a3a3a3" />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
{/* Left arrow - smooth curve */}
|
||||
<motion.path
|
||||
d="M 100 0 Q 150 60, 250 110"
|
||||
stroke="#d4d4d4"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
markerEnd="url(#arrowhead)"
|
||||
initial={{pathLength: 0}}
|
||||
whileInView={{pathLength: 1}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 1.2, delay: 0.4, ease: [0.22, 1, 0.36, 1]}}
|
||||
/>
|
||||
|
||||
{/* Center arrow - straight */}
|
||||
<motion.path
|
||||
d="M 300 0 L 300 110"
|
||||
stroke="#d4d4d4"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
markerEnd="url(#arrowhead)"
|
||||
initial={{pathLength: 0}}
|
||||
whileInView={{pathLength: 1}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 1.2, delay: 0.5, ease: [0.22, 1, 0.36, 1]}}
|
||||
/>
|
||||
|
||||
{/* Right arrow - smooth curve */}
|
||||
<motion.path
|
||||
d="M 500 0 Q 450 60, 350 110"
|
||||
stroke="#d4d4d4"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
markerEnd="url(#arrowhead)"
|
||||
initial={{pathLength: 0}}
|
||||
whileInView={{pathLength: 1}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 1.2, delay: 0.6, ease: [0.22, 1, 0.36, 1]}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Contact */}
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.8, delay: 0.9, ease: [0.22, 1, 0.36, 1]}}
|
||||
className={'mx-auto max-w-xl'}
|
||||
>
|
||||
<div className={'rounded-lg border border-neutral-200 bg-white'}>
|
||||
<div className={'flex items-center gap-6 p-8'}>
|
||||
<div
|
||||
className={'flex h-14 w-14 flex-shrink-0 items-center justify-center rounded-full bg-neutral-900'}
|
||||
>
|
||||
<User className="h-7 w-7 text-white" strokeWidth={1.5} />
|
||||
</div>
|
||||
<div className={'flex-1 space-y-1'}>
|
||||
<div className={'font-mono text-base font-medium text-neutral-900'}>[email protected]</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'grid grid-cols-3 gap-px border-t border-neutral-200 bg-neutral-200'}>
|
||||
<div className={'bg-white px-4 py-3 text-center'}>
|
||||
<div className={'text-lg font-bold text-neutral-900'}>1,247</div>
|
||||
<div className={'text-xs text-neutral-500'}>Emails received</div>
|
||||
</div>
|
||||
<div className={'bg-white px-4 py-3 text-center'}>
|
||||
<div className={'text-lg font-bold text-neutral-900'}>89%</div>
|
||||
<div className={'text-xs text-neutral-500'}>Open rate</div>
|
||||
</div>
|
||||
<div className={'bg-white px-4 py-3 text-center'}>
|
||||
<div className={'text-lg font-bold text-neutral-900'}>12</div>
|
||||
<div className={'text-xs text-neutral-500'}>Workflows</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Open Source */}
|
||||
<section className={'py-32'}>
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.7, ease: [0.22, 1, 0.36, 1]}}
|
||||
className={'mx-auto max-w-4xl text-center'}
|
||||
>
|
||||
<h2 className={'text-5xl font-bold tracking-tight text-neutral-900'}>Open source, privacy first</h2>
|
||||
<p className={'mt-6 text-lg text-neutral-600'}>
|
||||
Transparent codebase, privacy-focused infrastructure, and the option to self-host
|
||||
</p>
|
||||
|
||||
<div className={'mt-16 grid gap-px bg-neutral-200 sm:grid-cols-3'}>
|
||||
<div className={'bg-white p-12'}>
|
||||
<div className={'mx-auto flex h-14 w-14 items-center justify-center rounded-xl bg-neutral-100'}>
|
||||
<PackageOpen className={'h-7 w-7 text-neutral-900'} />
|
||||
</div>
|
||||
<h3 className={'mt-6 text-lg font-semibold text-neutral-900'}>Open Source</h3>
|
||||
<p className={'mt-2 text-sm text-neutral-600'}>AGPL-3.0 licensed</p>
|
||||
</div>
|
||||
<div className={'bg-white p-12'}>
|
||||
<div className={'mx-auto flex h-14 w-14 items-center justify-center rounded-xl bg-neutral-100'}>
|
||||
<Shield className={'h-7 w-7 text-neutral-900'} />
|
||||
</div>
|
||||
<h3 className={'mt-6 text-lg font-semibold text-neutral-900'}>Privacy First</h3>
|
||||
<p className={'mt-2 text-sm text-neutral-600'}>EU hosted</p>
|
||||
</div>
|
||||
<div className={'bg-white p-12'}>
|
||||
<div className={'mx-auto flex h-14 w-14 items-center justify-center rounded-xl bg-neutral-100'}>
|
||||
<Globe className={'h-7 w-7 text-neutral-900'} />
|
||||
</div>
|
||||
<h3 className={'mt-6 text-lg font-semibold text-neutral-900'}>Self-Hostable</h3>
|
||||
<p className={'mt-2 text-sm text-neutral-600'}>Deploy anywhere</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
|
||||
{/* Pricing */}
|
||||
<section className={'py-32'}>
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.7, ease: [0.22, 1, 0.36, 1]}}
|
||||
className={'mx-auto max-w-4xl text-center'}
|
||||
>
|
||||
<h2 className={'text-5xl font-bold tracking-tight text-neutral-900'}>Simple, transparent pricing</h2>
|
||||
<p className={'mt-6 text-lg text-neutral-600'}>Pay for what you use, nothing more</p>
|
||||
|
||||
<div className={'mt-20'}>
|
||||
<div className={'flex items-baseline justify-center gap-3'}>
|
||||
<span className={'text-7xl font-bold tracking-tight text-neutral-900'}>$0.001</span>
|
||||
<span className={'text-2xl text-neutral-500'}>/email</span>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mt-20 max-w-4xl">
|
||||
<div className="grid gap-8 sm:grid-cols-3">
|
||||
<div className="flex flex-col items-center gap-y-2">
|
||||
<svg role="img" className="h-8 w-8" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>MailChimp</title>
|
||||
<path d="M11.267 0C6.791-.015-1.82 10.246 1.397 12.964l.79.669a3.88 3.88 0 0 0-.22 1.792c.084.84.518 1.644 1.22 2.266.666.59 1.542.964 2.392.964 1.406 3.24 4.62 5.228 8.386 5.34 4.04.12 7.433-1.776 8.854-5.182.093-.24.488-1.316.488-2.267 0-.956-.54-1.352-.885-1.352-.01-.037-.078-.286-.172-.586-.093-.3-.19-.51-.19-.51.375-.563.382-1.065.332-1.35-.053-.353-.2-.653-.496-.964-.296-.311-.902-.63-1.753-.868l-.446-.124c-.002-.019-.024-1.053-.043-1.497-.014-.32-.042-.822-.197-1.315-.186-.668-.508-1.253-.911-1.627 1.112-1.152 1.806-2.422 1.804-3.511-.003-2.095-2.576-2.729-5.746-1.416l-.672.285A678.22 678.22 0 0 0 12.7.504C12.304.159 11.817.002 11.267 0zm.073.873c.166 0 .322.019.465.058.297.084 1.28 1.224 1.28 1.224s-1.826 1.013-3.52 2.426c-2.28 1.757-4.005 4.311-5.037 7.082-.811.158-1.526.618-1.963 1.253-.261-.218-.748-.64-.834-.804-.698-1.326.761-3.902 1.781-5.357C5.834 3.44 9.37.867 11.34.873zm3.286 3.273c.04-.002.06.05.028.074-.143.11-.299.26-.413.414a.04.04 0 0 0 .031.064c.659.004 1.587.235 2.192.574.041.023.012.103-.034.092-.915-.21-2.414-.369-3.97.01-1.39.34-2.45.863-3.224 1.426-.04.028-.086-.023-.055-.06.896-1.035 1.999-1.935 2.987-2.44.034-.018.07.019.052.052-.079.143-.23.447-.278.678-.007.035.032.063.062.042.615-.42 1.684-.868 2.622-.926zm3.023 3.205l.056.001a.896.896 0 0 1 .456.146c.534.355.61 1.216.638 1.845.015.36.059 1.229.074 1.478.034.571.184.651.487.751.17.057.33.098.563.164.706.198 1.125.4 1.39.658.157.162.23.333.253.497.083.608-.472 1.36-1.942 2.041-1.607.746-3.557.935-4.904.785l-.471-.053c-1.078-.145-1.693 1.247-1.046 2.201.417.615 1.552 1.015 2.688 1.015 2.604 0 4.605-1.111 5.35-2.072a.987.987 0 0 0 .06-.085c.036-.055.006-.085-.04-.054-.608.416-3.31 2.069-6.2 1.571 0 0-.351-.057-.672-.182-.255-.1-.788-.344-.853-.891 2.333.72 3.801.039 3.801.039a.072.072 0 0 0 .042-.072.067.067 0 0 0-.074-.06s-1.911.283-3.718-.378c.197-.64.72-.408 1.51-.345a11.045 11.045 0 0 0 3.647-.394c.818-.234 1.892-.697 2.727-1.356.281.618.38 1.299.38 1.299s.219-.04.4.073c.173.106.299.326.213.895-.176 1.063-.628 1.926-1.387 2.72a5.714 5.714 0 0 1-1.666 1.244c-.34.18-.704.334-1.087.46-2.863.935-5.794-.093-6.739-2.3a3.545 3.545 0 0 1-.189-.522c-.403-1.455-.06-3.2 1.008-4.299.065-.07.132-.153.132-.256 0-.087-.055-.179-.102-.243-.374-.543-1.669-1.466-1.409-3.254.187-1.284 1.31-2.189 2.357-2.135.089.004.177.01.266.015.453.027.85.085 1.223.1.625.028 1.187-.063 1.853-.618.225-.187.405-.35.71-.401.028-.005.092-.028.215-.028zm.022 2.18a.42.42 0 0 0-.06.005c-.335.054-.347.468-.228 1.04.068.32.187.595.32.765.175-.02.343-.022.498 0 .089-.205.104-.557.024-.942-.112-.535-.261-.872-.554-.868zm-3.66 1.546a1.724 1.724 0 0 0-1.016.326c-.16.117-.311.28-.29.378.008.032.031.056.088.063.131.015.592-.217 1.122-.25.374-.023.684.094.923.2.239.104.386.173.443.113.037-.038.026-.11-.031-.204-.118-.192-.36-.387-.618-.497a1.601 1.601 0 0 0-.621-.129zm4.082.81c-.171-.003-.313.186-.317.42-.004.236.131.43.303.432.172.003.314-.185.318-.42.004-.236-.132-.429-.304-.432zm-3.58.172c-.05 0-.102.002-.155.008-.311.05-.483.152-.593.247-.094.082-.152.173-.152.237a.075.075 0 0 0 .075.076c.07 0 .228-.063.228-.063a1.98 1.98 0 0 1 1.001-.104c.157.018.23.027.265-.026.01-.016.022-.049-.01-.1-.063-.103-.311-.269-.66-.275zm2.26.4c-.127 0-.235.051-.283.148-.075.154.035.363.246.466.21.104.443.063.52-.09.075-.155-.035-.364-.246-.467a.542.542 0 0 0-.237-.058zm-11.635.024c.048 0 .098 0 .149.003.73.04 1.806.6 2.052 2.19.217 1.41-.128 2.843-1.449 3.069-.123.02-.248.029-.374.026-1.22-.033-2.539-1.132-2.67-2.435-.145-1.44.591-2.548 1.894-2.811.117-.024.252-.04.398-.042zm-.07.927a1.144 1.144 0 0 0-.847.364c-.38.418-.439.988-.366 1.19.027.073.07.094.1.098.064.008.16-.039.22-.2a1.2 1.2 0 0 0 .017-.052 1.58 1.58 0 0 1 .157-.37.689.689 0 0 1 .955-.199c.266.174.369.5.255.81-.058.161-.154.469-.133.721.043.511.357.717.64.738.274.01.466-.143.515-.256.029-.067.005-.107-.011-.125-.043-.053-.113-.037-.18-.021a.638.638 0 0 1-.16.022.347.347 0 0 1-.294-.148c-.078-.12-.073-.3.013-.504.011-.028.025-.058.04-.092.138-.308.368-.825.11-1.317-.195-.37-.513-.602-.894-.65a1.135 1.135 0 0 0-.138-.01z"></path>
|
||||
</svg>
|
||||
<span className="text-sm font-medium">Mailchimp</span>
|
||||
<span className="text-sm text-neutral-500">$0,004 / email</span>
|
||||
<span className="text-sm text-neutral-500">Standard plan</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-y-2">
|
||||
<svg role="img" viewBox="0 0 24 24" className="h-8 w-8" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Twilio Sendgrid</title>
|
||||
<path d="M12 0C5.381-.008.008 5.352 0 11.971V12c0 6.64 5.359 12 12 12 6.64 0 12-5.36 12-12 0-6.641-5.36-12-12-12zm0 20.801c-4.846.015-8.786-3.904-8.801-8.75V12c-.014-4.846 3.904-8.786 8.75-8.801H12c4.847-.014 8.786 3.904 8.801 8.75V12c.015 4.847-3.904 8.786-8.75 8.801H12zm5.44-11.76c0 1.359-1.12 2.479-2.481 2.479-1.366-.007-2.472-1.113-2.479-2.479 0-1.361 1.12-2.481 2.479-2.481 1.361 0 2.481 1.12 2.481 2.481zm0 5.919c0 1.36-1.12 2.48-2.481 2.48-1.367-.008-2.473-1.114-2.479-2.48 0-1.359 1.12-2.479 2.479-2.479 1.361-.001 2.481 1.12 2.481 2.479zm-5.919 0c0 1.36-1.12 2.48-2.479 2.48-1.368-.007-2.475-1.113-2.481-2.48 0-1.359 1.12-2.479 2.481-2.479 1.358-.001 2.479 1.12 2.479 2.479zm0-5.919c0 1.359-1.12 2.479-2.479 2.479-1.367-.007-2.475-1.112-2.481-2.479 0-1.361 1.12-2.481 2.481-2.481 1.358 0 2.479 1.12 2.479 2.481z"></path>
|
||||
</svg>
|
||||
<span className="text-sm font-medium">Twilio Sendgrid</span>
|
||||
<span className="text-sm text-neutral-500">$0,002 / email</span>
|
||||
<span className="text-sm text-neutral-500">Essentials plan</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-y-2">
|
||||
<svg role="img" viewBox="0 0 24 24" className="h-8 w-8" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Mailgun</title>
|
||||
<path d="M11.837 0c6.602 0 11.984 5.381 11.984 11.994-.017 2.99-3.264 4.84-5.844 3.331a3.805 3.805 0 0 1-.06-.035l-.055-.033-.022.055c-2.554 4.63-9.162 4.758-11.894.232-2.732-4.527.46-10.313 5.746-10.416a6.868 6.868 0 0 1 7.002 6.866 1.265 1.265 0 0 0 2.52 0c0-5.18-4.197-9.38-9.377-9.387C4.611 2.594.081 10.41 3.683 16.673c3.238 5.632 11.08 6.351 15.289 1.402l1.997 1.686A11.95 11.95 0 0 1 11.837 24C2.6 23.72-2.87 13.543 1.992 5.684A12.006 12.006 0 0 1 11.837 0Zm0 7.745c-3.276-.163-5.5 3.281-4.003 6.2a4.26 4.26 0 0 0 4.014 2.31c3.276-.171 5.137-3.824 3.35-6.575a4.26 4.26 0 0 0-3.36-1.935Zm0 2.53c1.324 0 2.152 1.433 1.49 2.58a1.72 1.72 0 0 1-1.49.86 1.72 1.72 0 1 1 0-3.44Z"></path>
|
||||
</svg>
|
||||
<span className="text-sm font-medium">Mailgun</span>
|
||||
<span className="text-sm text-neutral-500">$0,003 / email</span>
|
||||
<span className="text-sm text-neutral-500">Foundation plan</span>
|
||||
</div>
|
||||
<p className="text-balance text-center text-xs text-neutral-400 sm:col-span-3">
|
||||
Calculated based on the plan that best matches the features of Plunk at 10.000 emails per month.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={'mt-16 flex flex-wrap justify-center gap-4'}>
|
||||
<Link
|
||||
href={'/pricing'}
|
||||
className={
|
||||
'rounded-lg border border-neutral-300 px-8 py-4 text-base font-semibold text-neutral-900 transition hover:border-neutral-400'
|
||||
}
|
||||
>
|
||||
View pricing details
|
||||
</Link>
|
||||
<a
|
||||
href={`${DASHBOARD_URI}/auth/signup`}
|
||||
className={
|
||||
'rounded-lg bg-neutral-900 px-8 py-4 text-base font-semibold text-white shadow-lg shadow-neutral-900/10 transition hover:bg-neutral-800'
|
||||
}
|
||||
>
|
||||
Start for free
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
|
||||
{/* Testimonials */}
|
||||
<section className={'py-32'}>
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.7, ease: [0.22, 1, 0.36, 1]}}
|
||||
>
|
||||
<div className={'mb-20 text-center'}>
|
||||
<h2 className={'text-5xl font-bold tracking-tight text-neutral-900'}>Trusted by the best</h2>
|
||||
</div>
|
||||
|
||||
<div className={'grid gap-px bg-neutral-200 sm:grid-cols-2 lg:grid-cols-3'}>
|
||||
{testimonials.map((t, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: index * 0.1, ease: [0.22, 1, 0.36, 1]}}
|
||||
className={'flex flex-col bg-white p-10'}
|
||||
>
|
||||
<p className={'min-h-[3.5rem] text-sm leading-relaxed text-neutral-600'}>
|
||||
“{t.testimonial}”
|
||||
</p>
|
||||
<div className={'mt-auto flex items-center gap-4 pt-6'}>
|
||||
<div className={'relative h-12 w-12 overflow-hidden rounded-full'}>
|
||||
<Image src={t.image} alt={t.author} placeholder="blur" className={'object-cover'} />
|
||||
</div>
|
||||
<div>
|
||||
<div className={'text-sm font-semibold text-neutral-900'}>{t.author}</div>
|
||||
<div className={'text-xs text-neutral-500'}>{t.role}</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className={'border-t border-neutral-200 py-32'}>
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.7, ease: [0.22, 1, 0.36, 1]}}
|
||||
className={'mx-auto max-w-3xl text-center'}
|
||||
>
|
||||
<h2 className={'text-5xl font-bold tracking-tight text-neutral-900'}>Ready to get started?</h2>
|
||||
<p className={'mt-6 text-lg text-neutral-600'}>
|
||||
Join thousands of businesses building better email experiences with Plunk
|
||||
</p>
|
||||
<div className={'mt-12 flex flex-wrap justify-center gap-4'}>
|
||||
<motion.a
|
||||
whileHover={{scale: 1.02}}
|
||||
whileTap={{scale: 0.98}}
|
||||
href={`${DASHBOARD_URI}/auth/signup`}
|
||||
className={
|
||||
'rounded-lg bg-neutral-900 px-8 py-4 text-base font-semibold text-white transition hover:bg-neutral-800'
|
||||
}
|
||||
>
|
||||
Create free account
|
||||
</motion.a>
|
||||
<Link
|
||||
href={WIKI_URI}
|
||||
target={'_blank'}
|
||||
className={
|
||||
'rounded-lg border border-neutral-300 px-8 py-4 text-base font-semibold text-neutral-900 transition hover:border-neutral-400'
|
||||
}
|
||||
>
|
||||
Read documentation
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,131 @@
|
||||
import {NextSeo} from 'next-seo';
|
||||
import React from 'react';
|
||||
import {Footer, Navbar} from '../components';
|
||||
import {motion} from 'framer-motion';
|
||||
|
||||
import {DASHBOARD_URI} from '../lib/constants';
|
||||
import {GithubIcon} from 'lucide-react';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default function Index() {
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
title={'Plunk Pricing | The Open-Source Email Platform'}
|
||||
description={
|
||||
'Transparent email pricing at $0.001 per email with no contact limits. Free plan includes 3,000 emails/month. No hidden fees, pay only for what you use.'
|
||||
}
|
||||
openGraph={{
|
||||
title: 'Plunk Pricing | The Open-Source Email Platform',
|
||||
description:
|
||||
'Transparent email pricing at $0.001 per email with no contact limits. Free plan includes 3,000 emails/month. No hidden fees, pay only for what you use.',
|
||||
}}
|
||||
additionalMetaTags={[
|
||||
{
|
||||
property: 'title',
|
||||
content: 'Plunk Pricing | The Open-Source Email Platform',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Navbar />
|
||||
|
||||
<div className={'mx-auto max-w-7xl px-8 sm:px-0'}>
|
||||
<main>
|
||||
<section className={'py-32'}>
|
||||
<div className={'mx-auto max-w-4xl text-center'}>
|
||||
<h1 className="text-5xl font-bold tracking-tight text-neutral-900 sm:text-6xl lg:text-7xl">
|
||||
Simple, transparent pricing
|
||||
</h1>
|
||||
<p className="mx-auto mt-6 max-w-2xl text-xl text-neutral-600">Start free, pay only for what you use</p>
|
||||
</div>
|
||||
|
||||
{/* Main Pricing Display */}
|
||||
<div className="mx-auto mt-20 max-w-2xl text-center">
|
||||
<div className="flex items-baseline justify-center gap-3">
|
||||
<span className="text-7xl font-bold tracking-tight text-neutral-900">$0.001</span>
|
||||
<span className="text-2xl text-neutral-500">/email</span>
|
||||
</div>
|
||||
|
||||
<motion.a
|
||||
href={`${DASHBOARD_URI}/auth/signup`}
|
||||
whileHover={{scale: 1.02}}
|
||||
whileTap={{scale: 0.98}}
|
||||
className={
|
||||
'mx-auto mt-10 inline-block rounded-lg bg-neutral-900 px-8 py-4 text-base font-semibold text-white transition hover:bg-neutral-800'
|
||||
}
|
||||
>
|
||||
Get started for free
|
||||
</motion.a>
|
||||
</div>
|
||||
|
||||
{/* Features Grid */}
|
||||
<div className="mx-auto mt-20 max-w-5xl">
|
||||
<h2 className="mb-12 text-center text-2xl font-bold text-neutral-900">Everything included</h2>
|
||||
<div className="grid gap-px bg-neutral-200 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="bg-white p-8">
|
||||
<h3 className="text-lg font-semibold text-neutral-900">Unlimited emails</h3>
|
||||
<p className="mt-2 text-sm text-neutral-600">
|
||||
Send unlimited transactional, campaign, and automated emails
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white p-8">
|
||||
<h3 className="text-lg font-semibold text-neutral-900">Unlimited contacts</h3>
|
||||
<p className="mt-2 text-sm text-neutral-600">
|
||||
No limits on your audience size. Grow without restrictions
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white p-8">
|
||||
<h3 className="text-lg font-semibold text-neutral-900">Custom domains</h3>
|
||||
<p className="mt-2 text-sm text-neutral-600">Send from your own domain with full authentication</p>
|
||||
</div>
|
||||
<div className="bg-white p-8">
|
||||
<h3 className="text-lg font-semibold text-neutral-900">Analytics & tracking</h3>
|
||||
<p className="mt-2 text-sm text-neutral-600">Detailed metrics on opens, clicks, and conversions</p>
|
||||
</div>
|
||||
<div className="bg-white p-8">
|
||||
<h3 className="text-lg font-semibold text-neutral-900">Workflow automation</h3>
|
||||
<p className="mt-2 text-sm text-neutral-600">Build complex email sequences with visual editor</p>
|
||||
</div>
|
||||
<div className="bg-white p-8">
|
||||
<h3 className="text-lg font-semibold text-neutral-900">API access</h3>
|
||||
<p className="mt-2 text-sm text-neutral-600">Full REST API with comprehensive documentation</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 overflow-hidden rounded-lg border border-neutral-200">
|
||||
<div className="flex flex-col items-center space-y-6 p-10 sm:flex-row sm:space-y-0">
|
||||
<div className="flex-1 text-center sm:text-left">
|
||||
<h2 className={'text-2xl font-bold text-neutral-900'}>Self-host</h2>
|
||||
<p className={'mt-2 text-neutral-600'}>
|
||||
Host Plunk on your own infrastructure. The perfect solution for when you require full control.
|
||||
</p>
|
||||
</div>
|
||||
<div className={'sm:ml-auto'}>
|
||||
<motion.button
|
||||
onClick={() => {
|
||||
window.open('https://github.com/useplunk/plunk', '_blank');
|
||||
}}
|
||||
whileHover={{scale: 1.02}}
|
||||
whileTap={{scale: 0.98}}
|
||||
className={
|
||||
'flex w-full items-center justify-center gap-x-3 rounded-lg bg-neutral-900 px-6 py-3 text-base font-semibold text-white transition hover:bg-neutral-800 sm:w-auto'
|
||||
}
|
||||
>
|
||||
<GithubIcon size={20} />
|
||||
View GitHub
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,166 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@plugin '@tailwindcss/forms';
|
||||
@plugin '@tailwindcss/typography';
|
||||
|
||||
@source '../../../../packages/ui/src/**/*.{ts,tsx}';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@utility container {
|
||||
margin-inline: auto;
|
||||
padding-inline: 2rem;
|
||||
@media (width >= --theme(--breakpoint-sm)) {
|
||||
max-width: none;
|
||||
}
|
||||
@media (width >= 1400px) {
|
||||
max-width: 1400px;
|
||||
}
|
||||
}
|
||||
|
||||
@theme {
|
||||
--font-*: initial;
|
||||
--font-sans: Inter, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
|
||||
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
|
||||
'Courier New', monospace;
|
||||
|
||||
--color-border: hsl(var(--border));
|
||||
--color-input: hsl(var(--input));
|
||||
--color-ring: hsl(var(--ring));
|
||||
--color-background: hsl(var(--background));
|
||||
--color-foreground: hsl(var(--foreground));
|
||||
|
||||
--color-primary: hsl(var(--primary));
|
||||
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||
|
||||
--color-secondary: hsl(var(--secondary));
|
||||
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||
|
||||
--color-destructive: hsl(var(--destructive));
|
||||
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
||||
|
||||
--color-muted: hsl(var(--muted));
|
||||
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||
|
||||
--color-accent: hsl(var(--accent));
|
||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||
|
||||
--color-popover: hsl(var(--popover));
|
||||
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||
|
||||
--color-card: hsl(var(--card));
|
||||
--color-card-foreground: hsl(var(--card-foreground));
|
||||
|
||||
--radius-lg: var(--radius);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentcolor);
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 100% 50%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: 215.4 16.3% 46.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 224 71% 4%;
|
||||
--foreground: 213 31% 91%;
|
||||
|
||||
--muted: 223 47% 11%;
|
||||
--muted-foreground: 215.4 16.3% 56.9%;
|
||||
|
||||
--accent: 216 34% 17%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 224 71% 4%;
|
||||
--popover-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--border: 216 34% 17%;
|
||||
--input: 216 34% 17%;
|
||||
|
||||
--card: 224 71% 4%;
|
||||
--card-foreground: 213 31% 91%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 1.2%;
|
||||
|
||||
--secondary: 222.2 47.4% 11.2%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 63% 31%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: 215.4 16.3% 56.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-neutral-800;
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user