Initial push of Plunk Next

This commit is contained in:
Dries Augustyns
2025-12-01 09:56:56 +01:00
parent 07cea20262
commit ff1876d580
566 changed files with 89036 additions and 28423 deletions
+5
View File
@@ -0,0 +1,5 @@
# URLS (Frontend - Next.js Public Variables)
NEXT_PUBLIC_API_URI=http://localhost:8080
NEXT_PUBLIC_DASHBOARD_URI=http://localhost:3000
NEXT_PUBLIC_LANDING_URI=http://localhost:4000
NEXT_PUBLIC_WIKI_URI=http://localhost:1000
+13
View File
@@ -0,0 +1,13 @@
import config from '@plunk/eslint-config/next';
const landingConfig = [
...config,
{
rules: {
'react/no-unescaped-entities': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
},
},
];
export default landingConfig;
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
+5
View File
@@ -0,0 +1,5 @@
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: process.env.NEXT_PUBLIC_LANDING_URI || 'https://www.swyp.be',
generateRobotsTxt: true,
};
+5
View File
@@ -0,0 +1,5 @@
/** @type {import('next').NextConfig} */
module.exports = {
transpilePackages: ['@plunk/ui'],
output: 'standalone', // Optimized for Docker
};
+43
View File
@@ -0,0 +1,43 @@
{
"name": "landing",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 4000",
"build": "next build",
"sitemap": "next-sitemap",
"start": "next start",
"lint": "eslint .",
"clean": "rimraf node_modules .next .turbo"
},
"dependencies": {
"@plunk/db": "*",
"@plunk/shared": "*",
"@plunk/ui": "*",
"lucide-react": "^0.553.0",
"next": "16.0.1",
"next-seo": "^6.6.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"sonner": "^2.0.6"
},
"devDependencies": {
"@plunk/eslint-config": "*",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.17",
"@tailwindcss/typography": "^0.5.19",
"@types/node": "24.10.0",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"eslint": "^9",
"eslint-config-next": "16.0.1",
"next-sitemap": "^4.2.3",
"postcss": "^8.4.33",
"tailwindcss": "^4.1.17",
"typescript": "5.7.2"
},
"resolutions": {
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2"
}
}
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
};
+4
View File
@@ -0,0 +1,4 @@
// Runtime environment configuration placeholder
// This file is overridden at Docker container startup with actual runtime values
// In development, the app falls back to NEXT_PUBLIC_* environment variables
window.__ENV__ = {};
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg viewBox="0 0 1080 1080" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1080" height="1080" fill="white"/>
<path d="M976 284.628C976 348.237 959.54 406.608 926.62 459.74C893.701 512.873 845.817 556.276 782.97 589.952C720.124 623.627 645.306 643.458 558.517 649.445L510.26 919.971C491.556 1023.99 440.68 1076 357.632 1076C311.993 1076 269.721 1062.53 230.816 1035.59C192.659 1008.65 161.984 967.49 138.79 912.113C115.597 856.736 104 788.637 104 707.816C104 555.902 128.316 427.187 176.947 321.671C226.327 215.407 292.166 136.082 374.466 83.6984C457.514 30.5661 548.791 4 648.299 4C718.627 4 778.107 16.3476 826.739 41.0429C876.118 65.7382 913.153 99.4136 937.843 142.069C963.281 183.976 976 231.496 976 284.628ZM578.718 533.826C732.094 514.369 808.783 434.671 808.783 294.731C808.783 245.34 792.323 205.304 759.403 174.622C727.231 143.192 677.103 127.476 609.019 127.476C531.957 127.476 464.621 151.798 407.012 200.44C350.15 249.082 306.008 316.807 274.584 403.615C243.909 489.674 228.571 588.081 228.571 698.836C228.571 745.233 233.06 786.392 242.039 822.312C251.765 858.232 263.736 886.295 277.951 906.501C292.915 925.957 307.13 935.686 320.597 935.686C339.302 935.686 353.517 909.868 363.243 858.232L400.278 646.078C371.099 641.587 358.38 639.717 362.121 640.465C339.676 636.723 325.086 629.988 318.353 620.26C311.619 609.783 308.252 596.687 308.252 580.972C308.252 564.508 312.741 551.412 321.719 541.684C331.446 531.955 344.539 527.091 360.999 527.091C368.481 527.091 374.092 527.465 377.833 528.214C395.789 531.207 409.63 533.078 419.357 533.826C429.083 475.455 442.924 397.254 460.88 299.221C465.369 273.777 475.47 255.817 491.182 245.34C507.641 234.115 526.72 228.503 548.417 228.503C573.107 228.503 590.689 233.367 601.163 243.095C612.386 252.075 617.997 266.668 617.997 286.873C617.997 298.847 617.249 308.575 615.753 316.059L578.718 533.826Z"
fill="black"/>
<path d="M304.835 467.541L426.099 489.952L411.851 580.937L391.091 699.816L266.088 676.643L304.835 467.541Z"
fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/favicon/mstile-150x150.png"/>
<TileColor>#ffffff</TileColor>
</tile>
</msapplication>
</browserconfig>
Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -0,0 +1,73 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1080.000000pt" height="1080.000000pt" viewBox="0 0 1080.000000 1080.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,1080.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M6218 10755 c-1 -1 -52 -6 -113 -9 -109 -6 -147 -10 -235 -21 -25 -3
-65 -8 -90 -11 -38 -5 -115 -17 -255 -40 -16 -3 -68 -14 -115 -24 -47 -10 -96
-21 -110 -23 -23 -5 -66 -16 -239 -64 -136 -37 -411 -134 -546 -193 -252 -109
-535 -257 -735 -384 -59 -38 -184 -121 -195 -129 -5 -4 -59 -44 -120 -88 -266
-197 -581 -493 -785 -739 -19 -23 -43 -52 -54 -64 -10 -11 -39 -48 -64 -81
-25 -33 -49 -62 -52 -65 -18 -14 -260 -357 -260 -367 0 -3 -10 -20 -23 -37
-69 -93 -387 -656 -392 -696 -1 -3 -11 -24 -23 -47 -108 -206 -293 -687 -382
-993 -12 -41 -26 -86 -30 -100 -5 -14 -19 -63 -30 -110 -12 -47 -28 -105 -35
-130 -8 -25 -16 -58 -19 -75 -3 -16 -14 -68 -26 -115 -11 -47 -25 -107 -30
-135 -6 -27 -12 -61 -15 -75 -3 -14 -7 -36 -10 -50 -2 -14 -9 -50 -15 -80 -6
-30 -13 -66 -16 -80 -2 -14 -7 -41 -10 -60 -7 -36 -9 -47 -18 -115 -3 -22 -8
-49 -10 -61 -2 -12 -7 -45 -11 -75 -3 -30 -8 -61 -10 -69 -2 -8 -6 -37 -10
-65 -3 -27 -7 -63 -9 -80 -2 -16 -7 -59 -10 -95 -4 -36 -9 -76 -11 -90 -2 -14
-7 -61 -11 -105 -4 -44 -8 -93 -10 -110 -3 -38 -14 -184 -19 -255 -27 -407
-27 -1218 0 -1455 2 -16 6 -64 10 -105 4 -41 9 -84 11 -95 2 -11 6 -46 9 -77
3 -32 10 -83 16 -115 5 -32 12 -76 15 -98 10 -68 73 -355 100 -450 237 -845
667 -1476 1235 -1810 46 -26 238 -121 299 -147 69 -30 263 -91 323 -103 15 -3
39 -7 55 -10 15 -3 32 -7 39 -10 6 -2 38 -7 70 -11 32 -3 62 -8 66 -11 15 -9
391 -12 452 -3 144 20 160 22 166 26 3 2 19 5 36 8 148 27 377 140 510 253
203 172 308 322 457 656 18 39 94 267 102 302 2 11 13 58 25 105 11 47 22 96
24 110 2 14 17 97 34 185 17 88 33 180 36 204 3 23 7 46 9 50 2 3 7 28 10 55
4 27 9 52 11 56 2 3 6 24 9 45 2 22 23 141 46 265 23 124 43 236 45 250 4 27
4 28 40 230 25 134 33 182 41 230 3 14 7 36 9 50 3 14 8 37 10 53 3 15 7 39
10 55 3 15 12 68 20 117 9 50 20 110 25 135 5 25 11 59 14 75 3 26 14 88 30
170 2 11 25 142 51 290 26 149 50 273 54 276 3 4 50 10 104 13 93 6 137 11
217 22 19 3 58 7 85 10 28 2 64 6 80 9 17 3 48 7 70 10 142 21 400 73 575 116
560 139 1036 346 1478 640 111 74 294 208 312 228 3 3 25 22 50 42 82 67 183
163 304 290 399 418 702 991 821 1549 11 55 36 196 44 255 40 283 33 767 -14
995 -2 14 -12 60 -20 102 -115 560 -455 1118 -898 1470 -328 261 -755 473
-1157 573 -81 20 -249 56 -305 66 -72 12 -78 12 -155 23 -25 4 -56 8 -70 11
-14 2 -61 7 -105 11 -44 3 -93 8 -110 11 -36 5 -672 13 -677 8z m142 -1239
c84 -6 217 -21 275 -31 11 -1 36 -6 55 -9 72 -12 87 -16 165 -35 367 -91 649
-262 871 -529 77 -92 74 -88 126 -172 84 -135 144 -280 187 -455 19 -77 24
-108 38 -225 14 -112 6 -513 -11 -608 -2 -9 -7 -42 -11 -72 -8 -63 -7 -54 -34
-185 -12 -55 -35 -145 -53 -200 -17 -55 -32 -102 -32 -105 -3 -17 -59 -142
-109 -240 -51 -100 -148 -252 -204 -321 -300 -366 -742 -625 -1309 -767 -142
-36 -267 -62 -359 -75 -16 -3 -46 -7 -65 -11 -106 -18 -102 -19 -96 22 14 88
18 112 32 192 9 47 17 97 19 112 4 27 21 122 30 168 3 14 8 43 11 65 2 22 18
119 35 215 16 96 32 189 35 205 5 33 15 89 20 120 2 11 15 88 29 170 14 83 34
200 45 260 11 61 22 126 24 145 3 19 9 60 15 90 11 59 17 92 27 155 3 22 12
74 20 115 8 41 17 91 19 110 3 19 9 64 14 100 10 69 7 336 -3 383 -54 239
-237 378 -531 404 -232 20 -416 -5 -580 -81 -223 -103 -343 -243 -414 -482
-16 -54 -85 -415 -135 -699 -15 -88 -58 -325 -76 -425 -12 -63 -23 -126 -25
-140 -2 -14 -13 -77 -25 -140 -11 -63 -23 -130 -26 -149 -3 -18 -7 -43 -10
-55 -4 -19 -56 -320 -68 -391 -3 -16 -12 -70 -21 -120 -9 -49 -18 -101 -20
-115 -2 -14 -22 -135 -44 -270 -22 -135 -52 -315 -65 -400 -14 -85 -28 -166
-30 -180 -16 -87 -69 -389 -71 -407 -2 -12 -6 -34 -9 -50 -3 -15 -8 -44 -11
-63 -4 -19 -10 -54 -16 -78 -5 -23 -12 -59 -15 -80 -5 -32 -37 -223 -50 -292
-2 -14 -7 -40 -10 -59 -6 -38 -11 -64 -19 -101 -3 -14 -7 -41 -9 -60 -3 -19
-19 -114 -36 -210 -17 -96 -33 -188 -36 -205 -3 -16 -7 -37 -9 -45 -2 -8 -6
-33 -10 -55 -3 -22 -12 -76 -20 -120 -8 -44 -17 -97 -21 -119 -3 -21 -8 -48
-10 -60 -2 -11 -29 -163 -59 -336 -68 -390 -62 -356 -90 -475 -147 -622 -384
-760 -719 -420 -129 131 -256 357 -354 632 -42 117 -104 320 -117 380 -2 10
-8 38 -14 63 -5 25 -12 59 -15 75 -3 17 -8 40 -10 52 -5 24 -34 226 -41 283
-2 19 -6 53 -9 75 -5 40 -10 107 -22 280 -18 274 -2 931 32 1290 13 146 34
338 39 375 2 14 9 61 15 105 6 44 13 95 16 114 2 19 6 43 9 55 2 12 7 38 10
59 5 35 50 280 59 322 2 11 12 56 21 100 69 335 236 874 359 1160 116 268 243
523 340 684 31 51 56 95 56 97 0 8 169 256 228 334 207 274 434 510 672 700
221 176 482 331 735 435 154 64 412 142 560 170 147 27 261 41 405 51 46 3 85
6 87 7 4 5 335 -1 423 -7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

@@ -0,0 +1,19 @@
{
"name": "Plunk",
"short_name": "Plunk",
"icons": [
{
"src": "/favicon/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/favicon/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
@@ -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">
&copy; {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';
+2
View File
@@ -0,0 +1,2 @@
export * from './Navbar';
export * from './Footer';
+8
View File
@@ -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';
+10
View File
@@ -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});
}
+15
View File
@@ -0,0 +1,15 @@
import useSWR from 'swr';
/**
*
*/
export function useStats() {
return useSWR<{
emails: number;
events: number;
projects: number;
contacts: number;
}>('/utils', {
shouldRetryOnError: false,
});
}
+32
View File
@@ -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;
+43
View File
@@ -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;
}
}
+74
View File
@@ -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>
);
}
+36
View File
@@ -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>
);
}
}
+15
View File
@@ -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 <></>;
}
+701
View File
@@ -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'}>
&ldquo;{t.testimonial}&rdquo;
</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
+131
View File
@@ -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
+166
View File
@@ -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;
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "@plunk/typescript-config/nextjs.json",
"include": ["next-env.d.ts", "next.config.js", "**/*.js", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}