feat: add copy-to-clipboard functionality and loading state to LicenseSuccess page

This commit is contained in:
Mauricio Siu 2025-03-23 13:31:05 -06:00
parent 6ac4e6b040
commit 89ee55a822
4 changed files with 158 additions and 79 deletions

View File

@ -1,31 +1,68 @@
"use client";
import { Container } from "@/components/Container";
import { SERVER_LICENSE_URL } from "@/components/pricing";
import { Button } from "@/components/ui/button";
import confetti from "canvas-confetti";
import { CheckCircle2, Copy, Mail, Terminal } from "lucide-react";
import copy from "copy-to-clipboard";
import { CheckCircle2, Copy, Loader2, Mail, Terminal } from "lucide-react";
import Link from "next/link";
import { redirect, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
interface LicenseSessionResponse {
type: "basic" | "professional" | "business";
billingType: "monthly" | "yearly";
key: string;
}
export default function LicenseSuccess() {
const [error, setError] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
// Generate a realistic-looking API key
const apiKey = `dk_live_${Array.from(
crypto.getRandomValues(new Uint8Array(24)),
)
.map((b) => b.toString(16).padStart(2, "0"))
.join("")}`;
const [loading, setLoading] = useState(true);
const query = useSearchParams();
const sessionId = query.get("session_id");
if (!sessionId) {
redirect("/");
}
const [data, setData] = useState<LicenseSessionResponse | null>(null);
useEffect(() => {
setLoading(true);
console.log(`${SERVER_LICENSE_URL}/license/session?sessionId=${sessionId}`);
fetch(`${SERVER_LICENSE_URL}/license/session?sessionId=${sessionId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
})
.then((res) => res.json())
.then((data) => setData(data))
.catch((err) => {
console.error(err);
setError(err.message);
})
.finally(() => {
setLoading(false);
});
}, [sessionId]);
useEffect(() => {
// Launch confetti when the page loads
confetti({
particleCount: 150,
spread: 100,
origin: { y: 0.6 },
});
}, []);
if (data) {
confetti({
particleCount: 150,
spread: 100,
origin: { y: 0.6 },
});
}
}, [data]);
const copyToClipboard = () => {
navigator.clipboard.writeText(apiKey);
copy(data?.key ?? "");
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
@ -33,86 +70,112 @@ export default function LicenseSuccess() {
return (
<div className="relative min-h-screen bg-gradient-to-b from-black via-zinc-900 to-black">
<div className="absolute inset-0 bg-[url('/grid.svg')] bg-center [mask-image:linear-gradient(180deg,white,rgba(255,255,255,0))]" />
<Container className="relative pt-24 pb-28">
<div className="mx-auto max-w-3xl text-center">
<div className="flex justify-center mb-8">
<div className="rounded-full bg-green-500/10 p-4">
<CheckCircle2 className="h-16 w-16 text-green-500" />
{loading ? (
<div className="relative pt-48 pb-28">
<div className="mx-auto max-w-4xl text-center flex justify-center items-center h-full">
<Loader2 className="h-16 w-16 text-zinc-500 animate-spin" />
</div>
</div>
) : (
<div className="relative pt-24 pb-28">
<div className="mx-auto max-w-4xl text-center">
<div className="flex justify-center mb-8">
<div className="rounded-full bg-green-500/10 p-4">
<CheckCircle2 className="h-16 w-16 text-green-500" />
</div>
</div>
</div>
<h1 className="text-5xl font-bold tracking-tight text-white sm:text-6xl mb-6">
Thank you for your purchase!
</h1>
<h1 className="text-5xl font-bold tracking-tight text-white sm:text-6xl mb-6">
Thank you for your purchase!
</h1>
<p className="text-xl leading-8 text-zinc-400 mb-6">
Your Dokploy license has been successfully activated. Here's your
API key to get started.
</p>
<div className="flex items-center justify-center gap-2 mb-12">
<Mail className="h-5 w-5 text-zinc-500" />
<p className="text-sm text-zinc-500">
We've also sent your API key to your email for safekeeping
<p className="text-xl leading-8 text-zinc-400 mb-6">
Your Dokploy license has been successfully activated. Here's your
API key to get started.
</p>
</div>
<div className="bg-black/50 backdrop-blur-sm border border-zinc-800 rounded-xl p-8 mb-12">
<div className="flex flex-col items-center space-y-6">
<Terminal className="h-10 w-10 text-zinc-500" />
<div className="space-y-4 w-full">
<div className="flex items-center justify-between space-x-4 bg-black/50 rounded-lg p-4 border border-zinc-800">
<code className="text-green-500 text-lg font-mono">
{apiKey}
</code>
<Button
variant="outline"
size="icon"
onClick={copyToClipboard}
className="transition-all duration-200 hover:bg-green-500/10 hover:text-green-500"
>
{copied ? (
<CheckCircle2 className="h-5 w-5" />
) : (
<Copy className="h-5 w-5" />
)}
</Button>
</div>
<div className="flex items-center justify-center gap-2 mb-12">
<Mail className="h-5 w-5 text-zinc-500" />
<p className="text-sm text-zinc-500">
We've also sent your API key to your email for safekeeping
</p>
</div>
<div className="text-left space-y-3">
<p className="text-zinc-400 text-sm">
To start using your license, add this API key to your
configuration file:
</p>
<pre className="bg-black/50 rounded-lg p-4 overflow-x-auto border border-zinc-800">
<code className="text-sm font-mono text-zinc-300">
{`# .env
DOKPLOY_LICENSE_KEY=${apiKey}`}
<div className="bg-black/50 backdrop-blur-sm border border-zinc-800 rounded-xl p-8 mb-12">
<div className="flex flex-col items-center space-y-6">
<Terminal className="h-10 w-10 text-zinc-500" />
<div className="space-y-4 w-full">
<div className="text-left space-y-3">
<p className="text-zinc-400 text-sm">
Steps to enable paid features
</p>
<ul>
<li>
<span>1. Web Server</span>
</li>
<li>
<span>2. Enable Paid Features</span>
</li>
<li>
<span>
3. Copy the Key Below and Paste it in the license key
field and click on validate
</span>
</li>
</ul>
</div>
<div className="flex items-center justify-between space-x-4 bg-black/50 rounded-lg p-4 border border-zinc-800">
<code className="text-green-500 text-lg font-mono">
{data?.key}
</code>
</pre>
<Button
variant="outline"
size="icon"
onClick={copyToClipboard}
className="transition-all duration-200 hover:bg-green-500/10 hover:text-green-500"
>
{copied ? (
<CheckCircle2 className="h-5 w-5" />
) : (
<Copy className="h-5 w-5" />
)}
</Button>
</div>
</div>
</div>
</div>
</div>
<div className="flex flex-col sm:flex-row justify-center gap-4">
<Link href="https://docs.dokploy.com/docs/core/installation">
<div className="flex flex-col sm:flex-row justify-center gap-4">
<Link href="https://docs.dokploy.com/docs/core/installation">
<Button className="w-full sm:w-auto hover:bg-zinc-800">
View Documentation
</Button>
</Link>
<Button
variant="outline"
className="w-full sm:w-auto hover:bg-zinc-800"
className="rounded-full bg-[#5965F2] hover:bg-[#4A55E0]"
asChild
>
View Documentation
<Link
href="https://discord.gg/2tBnJ3jDJc"
aria-label="Dokploy on GitHub"
target="_blank"
className="flex flex-row items-center gap-2 text-white"
>
<svg
role="img"
className="h-6 w-6 fill-white"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<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>
Discord
</Link>
</Button>
</Link>
<Link href="https://discord.gg/dokploy">
<Button className="w-full sm:w-auto bg-gradient-to-r from-indigo-500 to-purple-500 hover:from-indigo-600 hover:to-purple-600">
Join our Discord
</Button>
</Link>
</div>
</div>
</div>
</Container>
)}
</div>
);
}

View File

@ -50,7 +50,7 @@ function SwirlyDoodle(props: React.ComponentPropsWithoutRef<"svg">) {
);
}
const SERVER_LICENSE_URL =
export const SERVER_LICENSE_URL =
process.env.NODE_ENV === "development"
? "http://localhost:4002/api"
: "https://licenses.dokploy.com";

View File

@ -12,6 +12,7 @@
},
"browserslist": "defaults, not ie <= 11",
"dependencies": {
"copy-to-clipboard": "3.3.3",
"@headlessui/react": "^2.2.0",
"@headlessui/tailwindcss": "^0.2.0",
"@prettier/plugin-xml": "^3.4.1",

View File

@ -154,6 +154,9 @@ importers:
clsx:
specifier: ^2.1.0
version: 2.1.1
copy-to-clipboard:
specifier: 3.3.3
version: 3.3.3
framer-motion:
specifier: ^11.3.19
version: 11.3.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@ -2035,6 +2038,9 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
copy-to-clipboard@3.3.3:
resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
cosmiconfig-typescript-loader@5.0.0:
resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==}
engines: {node: '>=v16'}
@ -3643,6 +3649,9 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
toggle-selection@1.0.6:
resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==}
trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
@ -5780,6 +5789,10 @@ snapshots:
convert-source-map@2.0.0: {}
copy-to-clipboard@3.3.3:
dependencies:
toggle-selection: 1.0.6
cosmiconfig-typescript-loader@5.0.0(@types/node@20.17.16)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3):
dependencies:
'@types/node': 20.17.16
@ -7868,6 +7881,8 @@ snapshots:
dependencies:
is-number: 7.0.0
toggle-selection@1.0.6: {}
trim-lines@3.0.1: {}
trough@2.2.0: {}