mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: add pricing
This commit is contained in:
@@ -1,143 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import { ArrowRightIcon } from "lucide-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { calculatePrice } from "./show-billing";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
isAnnual: boolean;
|
|
||||||
serverQuantity: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ReviewPayment = ({ isAnnual, serverQuantity }: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const { data: billingSubscription } =
|
|
||||||
api.stripe.getBillingSubscription.useQuery();
|
|
||||||
|
|
||||||
const { data: calculateUpgradeCost } =
|
|
||||||
api.stripe.calculateUpgradeCost.useQuery(
|
|
||||||
{
|
|
||||||
serverQuantity,
|
|
||||||
isAnnual,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!serverQuantity && isOpen,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: calculateNewMonthlyCost } =
|
|
||||||
api.stripe.calculateNewMonthlyCost.useQuery(
|
|
||||||
{
|
|
||||||
serverQuantity,
|
|
||||||
isAnnual,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!serverQuantity && isOpen,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const isSameServersQty =
|
|
||||||
Number(billingSubscription?.totalServers) === serverQuantity;
|
|
||||||
|
|
||||||
const isSameCost =
|
|
||||||
Number(calculateNewMonthlyCost) ===
|
|
||||||
Number(billingSubscription?.monthlyAmount);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="outline">Review Payment</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-2xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Upgrade Plan</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
You are about to upgrade your plan to a{" "}
|
|
||||||
{isAnnual ? "annual" : "monthly"} plan. This will automatically
|
|
||||||
renew your subscription.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="flex flex-row w-full gap-4 items-center">
|
|
||||||
<div className="flex flex-col border gap-4 p-4 rounded-lg w-full">
|
|
||||||
<Label className="text-base font-semibold border-b border-b-divider pb-2">
|
|
||||||
Current Plan
|
|
||||||
</Label>
|
|
||||||
<div className="grid flex-1 gap-2">
|
|
||||||
<Label>Amount</Label>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
${billingSubscription?.monthlyAmount}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid flex-1 gap-2">
|
|
||||||
<Label>Servers</Label>
|
|
||||||
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{billingSubscription?.totalServers}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid flex-1 gap-2">
|
|
||||||
<Label>Next Payment</Label>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{billingSubscription?.nextPaymentDate
|
|
||||||
? format(billingSubscription?.nextPaymentDate, "MMM d, yyyy")
|
|
||||||
: "-"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="size-10">
|
|
||||||
<ArrowRightIcon className="size-6" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col border gap-4 p-4 rounded-lg w-full">
|
|
||||||
<Label className="text-base font-semibold border-b border-b-divider pb-2">
|
|
||||||
New Plan
|
|
||||||
</Label>
|
|
||||||
<div className="grid flex-1 gap-2">
|
|
||||||
<Label>Amount</Label>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
${calculatePrice(serverQuantity).toFixed(2)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid flex-1 gap-2">
|
|
||||||
<Label>Servers</Label>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{serverQuantity}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid flex-1 gap-2">
|
|
||||||
<Label>Difference</Label>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{isSameServersQty ? "-" : `$${calculateUpgradeCost} USD`}{" "}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid flex-1 gap-2">
|
|
||||||
<Label>New {isAnnual ? "annual" : "monthly"} cost</Label>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{isSameCost ? "-" : `$${calculateNewMonthlyCost} USD`}{" "}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter className="sm:justify-end">
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button type="button" variant="secondary">
|
|
||||||
Pay
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -181,7 +181,7 @@ export const ShowBilling = () => {
|
|||||||
<NumberInput
|
<NumberInput
|
||||||
value={serverQuantity}
|
value={serverQuantity}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setServerQuantity(e.target.value);
|
setServerQuantity(e.target.value as unknown as number);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { MinusIcon, PlusIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Container } from "./Container";
|
import { Container } from "./Container";
|
||||||
import { trackGAEvent } from "./analitycs";
|
import { trackGAEvent } from "./analitycs";
|
||||||
import { Button } from "./ui/button";
|
import { Badge } from "./ui/badge";
|
||||||
import { Switch } from "./ui/switch";
|
import { Button, buttonVariants } from "./ui/button";
|
||||||
|
import { NumberInput } from "./ui/input";
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
|
||||||
|
|
||||||
function SwirlyDoodle(props: React.ComponentPropsWithoutRef<"svg">) {
|
function SwirlyDoodle(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||||
return (
|
return (
|
||||||
@@ -55,7 +59,14 @@ function CheckIcon({
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export const calculatePrice = (count: number, isAnnual = false) => {
|
||||||
|
if (isAnnual) {
|
||||||
|
if (count <= 1) return 45.9;
|
||||||
|
return 35.7 * count;
|
||||||
|
}
|
||||||
|
if (count <= 1) return 4.5;
|
||||||
|
return count * 3.5;
|
||||||
|
};
|
||||||
function Plan({
|
function Plan({
|
||||||
name,
|
name,
|
||||||
price,
|
price,
|
||||||
@@ -63,6 +74,7 @@ function Plan({
|
|||||||
href,
|
href,
|
||||||
features,
|
features,
|
||||||
featured = false,
|
featured = false,
|
||||||
|
buttonText = "Get Started",
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
price: string;
|
price: string;
|
||||||
@@ -70,6 +82,7 @@ function Plan({
|
|||||||
href: string;
|
href: string;
|
||||||
features: Array<string>;
|
features: Array<string>;
|
||||||
featured?: boolean;
|
featured?: boolean;
|
||||||
|
buttonText?: string;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
@@ -116,23 +129,17 @@ function Plan({
|
|||||||
}}
|
}}
|
||||||
className="rounded-full mt-8"
|
className="rounded-full mt-8"
|
||||||
>
|
>
|
||||||
Get started
|
{buttonText}
|
||||||
</Button>
|
</Button>
|
||||||
{/* <Button
|
|
||||||
href={href}
|
|
||||||
variant={featured ? "solid" : "outline"}
|
|
||||||
color="white"
|
|
||||||
className="mt-8"
|
|
||||||
aria-label={`Get started with the ${name} plan for ${price}`}
|
|
||||||
>
|
|
||||||
Get started
|
|
||||||
</Button> */}
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Pricing() {
|
export function Pricing() {
|
||||||
const [monthly, setMonthly] = useState(false);
|
const router = useRouter();
|
||||||
|
const [isAnnual, setIsAnnual] = useState(true);
|
||||||
|
const [serverQuantity, setServerQuantity] = useState(3);
|
||||||
|
const featured = true;
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id="pricing"
|
id="pricing"
|
||||||
@@ -152,54 +159,200 @@ export function Pricing() {
|
|||||||
Deploy Smarter, Scale Faster – Without Breaking the Bank
|
Deploy Smarter, Scale Faster – Without Breaking the Bank
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-10 flex flex-row gap-x-4 justify-center">
|
|
||||||
<Switch checked={monthly} onCheckedChange={(e) => setMonthly(e)} />
|
|
||||||
{!monthly ? "Monthly" : "Yearly"}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className=" mt-10 mx-auto">
|
<div className=" mt-10 mx-auto">
|
||||||
<div className="mt-16 grid md:grid-cols-2 gap-y-10 mx-auto w-full lg:-mx-8 xl:mx-0 xl:gap-x-8">
|
<div className="mt-16 flex flex-col gap-10 mx-auto w-full lg:-mx-8 xl:mx-0 xl:gap-x-8 justify-center items-center">
|
||||||
<Plan
|
<Tabs
|
||||||
name="Free"
|
defaultValue="monthly"
|
||||||
price="$0"
|
value={isAnnual ? "annual" : "monthly"}
|
||||||
description="Perfect for developers who prefer to manage their own servers."
|
// className="w-full"
|
||||||
href="https://docs.dokploy.com/en/docs/core/get-started/installation#docker"
|
onValueChange={(e) => setIsAnnual(e === "annual")}
|
||||||
features={[
|
>
|
||||||
"Unlimited deployments",
|
<TabsList>
|
||||||
"Self-hosted on your own infrastructure",
|
<TabsTrigger value="monthly">Monthly</TabsTrigger>
|
||||||
"Full access to all deployment features",
|
<TabsTrigger value="annual">Annual</TabsTrigger>
|
||||||
"Docker Swarm and Docker Compose support",
|
</TabsList>
|
||||||
"Community support",
|
</Tabs>
|
||||||
"Custom domains and SSL integration",
|
<div className="flex flex-row max-w-4xl gap-4 mx-auto">
|
||||||
"No feature limitations on the core platform",
|
<section
|
||||||
]}
|
className={clsx(
|
||||||
/>
|
"flex flex-col rounded-3xl border-dashed border-muted border-2 px-4 max-w-sm",
|
||||||
<Plan
|
featured
|
||||||
featured
|
? "order-first bg-black border py-8 lg:order-none"
|
||||||
name="General"
|
: "lg:py-8",
|
||||||
price={!monthly ? "$5.99" : "$4.49"}
|
)}
|
||||||
description="Ideal for indie hackers, freelancers, agencies, and businesses looking for a managed solution."
|
>
|
||||||
href="/register"
|
<div className="flex flex-row gap-2 items-center">
|
||||||
features={[
|
<p className=" text-2xl font-semibold tracking-tight text-primary ">
|
||||||
"2 free server included (user-provided)",
|
Free
|
||||||
"All self-hosted features without hosting the UI",
|
</p>
|
||||||
"Dokploy infrastructure managed by us",
|
|
|
||||||
"$3.50 per additional server (user-provided)",
|
<p className=" text-base font-semibold tracking-tight text-muted-foreground">
|
||||||
]}
|
Open Source
|
||||||
/>
|
</p>
|
||||||
{/* <Plan
|
</div>
|
||||||
name="Enterprise"
|
|
||||||
price="$39"
|
<h3 className="mt-5 font-medium text-lg text-white">
|
||||||
description="For even the biggest enterprise companies."
|
Dokploy Open Source
|
||||||
href="/register"
|
</h3>
|
||||||
features={[
|
<p
|
||||||
"Send unlimited quotes and invoices",
|
className={clsx(
|
||||||
"Connect up to 15 bank accounts",
|
"text-sm",
|
||||||
"Track up to 200 expenses per month",
|
featured ? "text-white" : "text-slate-400",
|
||||||
"Automated payroll support",
|
)}
|
||||||
"Export up to 25 reports, including TPS",
|
>
|
||||||
]}
|
Manager your own infrastructure installing dokploy ui in your
|
||||||
/> */}
|
own server.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
role="list"
|
||||||
|
className={clsx(
|
||||||
|
" mt-4 flex flex-col gap-y-2 text-sm",
|
||||||
|
featured ? "text-white" : "text-slate-200",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
"Complete Flexibility: Install Dokploy UI on your own infrastructure",
|
||||||
|
"Unlimited Deployments",
|
||||||
|
"Self-hosted Infrastructure",
|
||||||
|
"Community Support",
|
||||||
|
"Access to Core Features",
|
||||||
|
"Dokploy Integration",
|
||||||
|
"Basic Backups",
|
||||||
|
"Access to All Updates",
|
||||||
|
].map((feature) => (
|
||||||
|
<li key={feature} className="flex text-muted-foreground">
|
||||||
|
<CheckIcon />
|
||||||
|
<span className="ml-2">{feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="flex flex-col gap-2 mt-4">
|
||||||
|
<div className="flex items-center gap-2 justify-center">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Unlimited Servers
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
className={clsx(
|
||||||
|
"flex flex-col rounded-3xl border-dashed border-2 px-4 max-w-sm",
|
||||||
|
featured
|
||||||
|
? "order-first bg-black border py-8 lg:order-none"
|
||||||
|
: "lg:py-8",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-row gap-2 items-center mb-4">
|
||||||
|
<Badge>Recommended 🚀</Badge>
|
||||||
|
</div>
|
||||||
|
{isAnnual ? (
|
||||||
|
<div className="flex flex-row gap-2 items-center">
|
||||||
|
<p className=" text-2xl font-semibold tracking-tight text-primary ">
|
||||||
|
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)}{" "}
|
||||||
|
USD
|
||||||
|
</p>
|
||||||
|
|
|
||||||
|
<p className=" text-base font-semibold tracking-tight text-muted-foreground">
|
||||||
|
${" "}
|
||||||
|
{(calculatePrice(serverQuantity, isAnnual) / 12).toFixed(
|
||||||
|
2,
|
||||||
|
)}{" "}
|
||||||
|
/ Month USD
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className=" text-2xl font-semibold tracking-tight text-primary ">
|
||||||
|
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<h3 className="mt-5 font-medium text-lg text-white">
|
||||||
|
Dokploy Plan
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
className={clsx(
|
||||||
|
"text-sm",
|
||||||
|
featured ? "text-white" : "text-slate-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
No need to manage Dokploy UI infrastructure, we take care of
|
||||||
|
it for you.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
role="list"
|
||||||
|
className={clsx(
|
||||||
|
" mt-4 flex flex-col gap-y-2 text-sm",
|
||||||
|
featured ? "text-white" : "text-slate-200",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
"Managed Hosting: No need to manage your own servers",
|
||||||
|
"Priority Support",
|
||||||
|
"Future-Proof Features",
|
||||||
|
].map((feature) => (
|
||||||
|
<li key={feature} className="flex text-muted-foreground">
|
||||||
|
<CheckIcon />
|
||||||
|
<span className="ml-2">{feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="flex flex-col gap-2 mt-4">
|
||||||
|
<div className="flex items-center gap-2 justify-center">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{serverQuantity} Servers (You bring the servers)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
disabled={serverQuantity <= 1}
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
if (serverQuantity <= 1) return;
|
||||||
|
|
||||||
|
setServerQuantity(serverQuantity - 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MinusIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<NumberInput
|
||||||
|
value={serverQuantity}
|
||||||
|
onChange={(e) => {
|
||||||
|
setServerQuantity(e.target.value as unknown as number);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setServerQuantity(serverQuantity + 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"justify-between",
|
||||||
|
// : "justify-end",
|
||||||
|
"flex flex-row items-center gap-2 mt-4",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="justify-end w-full">
|
||||||
|
<Link
|
||||||
|
href="https://app.dokploy.com/register"
|
||||||
|
target="_blank"
|
||||||
|
className={buttonVariants({ className: "w-full" })}
|
||||||
|
>
|
||||||
|
Subscribe
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
@@ -212,48 +365,43 @@ export function Pricing() {
|
|||||||
const faqs = [
|
const faqs = [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
question: "How does Dokploy's free plan work?",
|
question: "How does Dokploy's Open Source plan work?",
|
||||||
answer:
|
answer:
|
||||||
"The free plan allows you to self-host Dokploy on your own infrastructure with unlimited deployments and full access to all features.",
|
"You can host Dokploy UI on your own infrastructure and you will be responsible for the maintenance and updates.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "Do I need to provide my own server for the managed plan?",
|
question: "Do I need to provide my own server for the managed plan?",
|
||||||
answer:
|
answer:
|
||||||
"Yes, in the managed plan, you provide your own server, and we manage the Dokploy UI infrastructure for you.",
|
"Yes, in the managed plan, you provide your own server eg(Hetzner, Hostinger, AWS, ETC.) VPS, and we manage the Dokploy UI infrastructure for you.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "What happens if I need more than one server?",
|
question: "What happens if I need more than one server?",
|
||||||
answer:
|
answer:
|
||||||
"Each additional server costs $3.99/month and can be easily added to your account.",
|
"The first server costs $4.50/month, if you buy more than one it will be $3.50/month per server.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{
|
|
||||||
question: "Can I use my custom domain with Dokploy?",
|
|
||||||
answer:
|
|
||||||
"Yes, custom domain support is available on all plans, including the free version.",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
question: "Is there a limit on the number of deployments?",
|
question: "Is there a limit on the number of deployments?",
|
||||||
answer:
|
answer:
|
||||||
"No, there is no limit on the number of deployments in any of the plans.",
|
"No, there is no limit on the number of deployments in any of the plans.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "Do I have to manually configure Traefik?",
|
question: "What happens if I exceed my purchased server limit?",
|
||||||
answer:
|
answer:
|
||||||
"Dokploy offers dynamic Traefik configuration out-of-the-box, so no manual setup is needed.",
|
"The most recently added servers will be deactivated. You won't be able to create services on inactive servers until they are reactivated.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Do you offer a refunds?",
|
||||||
|
answer:
|
||||||
|
"We do not offer refunds. However, you can cancel your subscription at any time. Feel free to try our open-source version for free before making a purchase.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{
|
|
||||||
question: "How do automated backups work?",
|
|
||||||
answer:
|
|
||||||
"Automated backups are included in the managed plan and are limited to database backups only.",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
question: "What kind of support do you offer?",
|
question: "What kind of support do you offer?",
|
||||||
answer:
|
answer:
|
||||||
"We offer community support for the free plan and priority support for paid plans.",
|
"We offer community support for the open source version and priority support for paid plans.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "Is Dokploy open-source?",
|
question: "Is Dokploy open-source?",
|
||||||
@@ -279,7 +427,7 @@ export function Faqs() {
|
|||||||
{"Frequently asked questions"}
|
{"Frequently asked questions"}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-lg tracking-tight text-muted-foreground">
|
<p className="mt-4 text-lg tracking-tight text-muted-foreground">
|
||||||
If you can’t find what you’re looking for, please submit an issue
|
If you can’t find what you’re looking for, please send us an email
|
||||||
to:{" "}
|
to:{" "}
|
||||||
<Link href={"mailto:support@dokploy.com"} className="text-primary">
|
<Link href={"mailto:support@dokploy.com"} className="text-primary">
|
||||||
support@dokploy.com
|
support@dokploy.com
|
||||||
|
|||||||
36
apps/website/components/ui/badge.tsx
Normal file
36
apps/website/components/ui/badge.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { type VariantProps, cva } from "class-variance-authority";
|
||||||
|
import type * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||||
|
outline: "text-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants };
|
||||||
69
apps/website/components/ui/input.tsx
Normal file
69
apps/website/components/ui/input.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, errorMessage, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
// bg-gray
|
||||||
|
"flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{errorMessage && (
|
||||||
|
<span className="text-sm text-red-600 text-secondary-foreground">
|
||||||
|
{errorMessage}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Input.displayName = "Input";
|
||||||
|
|
||||||
|
const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, errorMessage, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
className={cn("text-left", className)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
value={props.value === undefined ? undefined : String(props.value)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value === "") {
|
||||||
|
props.onChange?.(e);
|
||||||
|
} else {
|
||||||
|
const number = Number.parseInt(value, 10);
|
||||||
|
if (!Number.isNaN(number)) {
|
||||||
|
const syntheticEvent = {
|
||||||
|
...e,
|
||||||
|
target: {
|
||||||
|
...e.target,
|
||||||
|
value: number,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
props.onChange?.(
|
||||||
|
syntheticEvent as unknown as React.ChangeEvent<HTMLInputElement>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
NumberInput.displayName = "NumberInput";
|
||||||
|
|
||||||
|
export { Input, NumberInput };
|
||||||
53
apps/website/components/ui/tabs.tsx
Normal file
53
apps/website/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root;
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||||
@@ -24,9 +24,9 @@
|
|||||||
"projectsDes": "Manage and organize all your projects in one place, keeping detailed track of progress and resource allocation.",
|
"projectsDes": "Manage and organize all your projects in one place, keeping detailed track of progress and resource allocation.",
|
||||||
"applications": "Applications & Databases",
|
"applications": "Applications & Databases",
|
||||||
"applicationsDes": "Centralize control over your applications and databases for enhanced security and efficiency, simplifying access and management across your infrastructure.",
|
"applicationsDes": "Centralize control over your applications and databases for enhanced security and efficiency, simplifying access and management across your infrastructure.",
|
||||||
"compose": "compose",
|
"compose": "Compose",
|
||||||
"composeDes": "Native Docker Compose support for manage complex applications and services with ease.",
|
"composeDes": "Native Docker Compose support for manage complex applications and services with ease.",
|
||||||
"multinode": "multinode",
|
"multinode": "Multinode",
|
||||||
"multinodeDes": "Scale applications to multiples nodes using docker swarm to manage the cluster.",
|
"multinodeDes": "Scale applications to multiples nodes using docker swarm to manage the cluster.",
|
||||||
"monitoring": "Monitoring",
|
"monitoring": "Monitoring",
|
||||||
"monitoringDes": "Monitor your systems' performance and health in real time, ensuring continuous and uninterrupted operation.",
|
"monitoringDes": "Monitor your systems' performance and health in real time, ensuring continuous and uninterrupted operation.",
|
||||||
|
|||||||
Reference in New Issue
Block a user