diff --git a/apps/dokploy/components/dashboard/settings/billing/review-payment.tsx b/apps/dokploy/components/dashboard/settings/billing/review-payment.tsx deleted file mode 100644 index 01be719e..00000000 --- a/apps/dokploy/components/dashboard/settings/billing/review-payment.tsx +++ /dev/null @@ -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 ( - - - - - - - Upgrade Plan - - You are about to upgrade your plan to a{" "} - {isAnnual ? "annual" : "monthly"} plan. This will automatically - renew your subscription. - - -
-
- -
- - - ${billingSubscription?.monthlyAmount} - -
-
- - - - {billingSubscription?.totalServers} - -
-
- - - {billingSubscription?.nextPaymentDate - ? format(billingSubscription?.nextPaymentDate, "MMM d, yyyy") - : "-"} - -
-
-
- -
-
- -
- - - ${calculatePrice(serverQuantity).toFixed(2)} - -
-
- - - {serverQuantity} - -
-
- - - {isSameServersQty ? "-" : `$${calculateUpgradeCost} USD`}{" "} - -
-
- - - {isSameCost ? "-" : `$${calculateNewMonthlyCost} USD`}{" "} - -
-
-
- - - - - - -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx index acac4fdf..68275a21 100644 --- a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx +++ b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx @@ -181,7 +181,7 @@ export const ShowBilling = () => { { - setServerQuantity(e.target.value); + setServerQuantity(e.target.value as unknown as number); }} /> diff --git a/apps/website/components/pricing.tsx b/apps/website/components/pricing.tsx index f3266df4..508e5eb2 100644 --- a/apps/website/components/pricing.tsx +++ b/apps/website/components/pricing.tsx @@ -1,13 +1,17 @@ "use client"; import clsx from "clsx"; +import { cn } from "@/lib/utils"; +import { MinusIcon, PlusIcon } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { Container } from "./Container"; import { trackGAEvent } from "./analitycs"; -import { Button } from "./ui/button"; -import { Switch } from "./ui/switch"; +import { Badge } from "./ui/badge"; +import { Button, buttonVariants } from "./ui/button"; +import { NumberInput } from "./ui/input"; +import { Tabs, TabsList, TabsTrigger } from "./ui/tabs"; function SwirlyDoodle(props: React.ComponentPropsWithoutRef<"svg">) { return ( @@ -55,7 +59,14 @@ function CheckIcon({ ); } - +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({ name, price, @@ -63,6 +74,7 @@ function Plan({ href, features, featured = false, + buttonText = "Get Started", }: { name: string; price: string; @@ -70,6 +82,7 @@ function Plan({ href: string; features: Array; featured?: boolean; + buttonText?: string; }) { const router = useRouter(); return ( @@ -116,23 +129,17 @@ function Plan({ }} className="rounded-full mt-8" > - Get started + {buttonText} - {/* */} ); } 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 (
-
- setMonthly(e)} /> - {!monthly ? "Monthly" : "Yearly"} -
-
- - - {/* */} +
+ setIsAnnual(e === "annual")} + > + + Monthly + Annual + + +
+
+
+

+ Free +

+ | +

+ Open Source +

+
+ +

+ Dokploy Open Source +

+

+ Manager your own infrastructure installing dokploy ui in your + own server. +

+ +
    + {[ + "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) => ( +
  • + + {feature} +
  • + ))} +
+
+
+ + Unlimited Servers + +
+
+
+
+
+ Recommended 🚀 +
+ {isAnnual ? ( +
+

+ $ {calculatePrice(serverQuantity, isAnnual).toFixed(2)}{" "} + USD +

+ | +

+ ${" "} + {(calculatePrice(serverQuantity, isAnnual) / 12).toFixed( + 2, + )}{" "} + / Month USD +

+
+ ) : ( +

+ $ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD +

+ )} +

+ Dokploy Plan +

+

+ No need to manage Dokploy UI infrastructure, we take care of + it for you. +

+ +
    + {[ + "Managed Hosting: No need to manage your own servers", + "Priority Support", + "Future-Proof Features", + ].map((feature) => ( +
  • + + {feature} +
  • + ))} +
+
+
+ + {serverQuantity} Servers (You bring the servers) + +
+ +
+ + { + setServerQuantity(e.target.value as unknown as number); + }} + /> + + +
+
+
+ + Subscribe + +
+
+
+
+
@@ -212,48 +365,43 @@ export function Pricing() { const faqs = [ [ { - question: "How does Dokploy's free plan work?", + question: "How does Dokploy's Open Source plan work?", 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?", 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?", 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?", answer: "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: - "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?", 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?", @@ -279,7 +427,7 @@ export function Faqs() { {"Frequently asked questions"}

- 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:{" "} support@dokploy.com diff --git a/apps/website/components/ui/badge.tsx b/apps/website/components/ui/badge.tsx new file mode 100644 index 00000000..f38976c0 --- /dev/null +++ b/apps/website/components/ui/badge.tsx @@ -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, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +

+ ); +} + +export { Badge, badgeVariants }; diff --git a/apps/website/components/ui/input.tsx b/apps/website/components/ui/input.tsx new file mode 100644 index 00000000..8fe7ab28 --- /dev/null +++ b/apps/website/components/ui/input.tsx @@ -0,0 +1,69 @@ +import { cn } from "@/lib/utils"; +import * as React from "react"; + +export interface InputProps + extends React.InputHTMLAttributes { + errorMessage?: string; +} + +const Input = React.forwardRef( + ({ className, errorMessage, type, ...props }, ref) => { + return ( + <> + + {errorMessage && ( + + {errorMessage} + + )} + + ); + }, +); +Input.displayName = "Input"; + +const NumberInput = React.forwardRef( + ({ className, errorMessage, ...props }, ref) => { + return ( + { + 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, + ); + } + } + }} + /> + ); + }, +); +NumberInput.displayName = "NumberInput"; + +export { Input, NumberInput }; diff --git a/apps/website/components/ui/tabs.tsx b/apps/website/components/ui/tabs.tsx new file mode 100644 index 00000000..e54c215a --- /dev/null +++ b/apps/website/components/ui/tabs.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/apps/website/locales/en.json b/apps/website/locales/en.json index d171e3c7..7c401706 100644 --- a/apps/website/locales/en.json +++ b/apps/website/locales/en.json @@ -24,9 +24,9 @@ "projectsDes": "Manage and organize all your projects in one place, keeping detailed track of progress and resource allocation.", "applications": "Applications & Databases", "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.", - "multinode": "multinode", + "multinode": "Multinode", "multinodeDes": "Scale applications to multiples nodes using docker swarm to manage the cluster.", "monitoring": "Monitoring", "monitoringDes": "Monitor your systems' performance and health in real time, ensuring continuous and uninterrupted operation.",