From 29aae919593b90ba40555deb7ee9877d0c09e4ba Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 31 Jul 2025 03:35:48 -0600 Subject: [PATCH] feat(dashboard): implement form handling for server selection with Zod validation and enhance UI components --- .../billing/show-hostinger-servers.tsx | 470 +++++++++++++----- 1 file changed, 352 insertions(+), 118 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/billing/show-hostinger-servers.tsx b/apps/dokploy/components/dashboard/settings/billing/show-hostinger-servers.tsx index a62098bc8..f6f928990 100644 --- a/apps/dokploy/components/dashboard/settings/billing/show-hostinger-servers.tsx +++ b/apps/dokploy/components/dashboard/settings/billing/show-hostinger-servers.tsx @@ -1,3 +1,4 @@ +import { zodResolver } from "@hookform/resolvers/zod"; import { Calendar, Cpu, @@ -8,7 +9,10 @@ import { MemoryStick, Server, } from "lucide-react"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Card, CardContent, @@ -16,9 +20,35 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { api } from "@/utils/api"; +const formSchema = z.object({ + datacenter: z.string({ + required_error: "Please select a datacenter", + }), + plan: z.string({ + required_error: "Please select a server plan", + }), + billingPeriod: z.object( + { + unit: z.enum(["month", "year"]), + period: z.number(), + }, + { + required_error: "Please select a billing period", + }, + ), +}); + // Format billing period function formatBillingPeriod(period: number, unit: string): string { if (unit === "month") { @@ -30,13 +60,45 @@ function formatBillingPeriod(period: number, unit: string): string { return `${period} ${unit}`; } -// Calculate yearly savings -function calculateSavings(monthlyPrice: number, yearlyPrice: number): number { - return monthlyPrice * 12 - yearlyPrice; +// Convert price from cents to dollars +function formatPrice(priceInCents: number): string { + return (priceInCents / 100).toFixed(2); } +// Calculate yearly savings +function calculateSavings( + monthlyPriceInCents: number, + yearlyPriceInCents: number, +): number { + return (monthlyPriceInCents * 12 - yearlyPriceInCents) / 100; +} + +type FormData = z.infer; + export const ShowHostingerServers = () => { - const { data: vpsPlans, isLoading } = api.hostinger.vpsPlans.useQuery(); + const { data: vpsPlans, isLoading: plansLoading } = + api.hostinger.vpsPlans.useQuery(); + const { data: dataCenters, isLoading: centersLoading } = + api.hostinger.dataCenters.useQuery(); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + datacenter: "", + plan: "", + billingPeriod: { + unit: "month", + period: 1, + }, + }, + }); + + const isLoading = plansLoading || centersLoading; + + function onSubmit(data: FormData) { + console.log(data); + // Handle form submission here + } if (isLoading) { return ( @@ -66,122 +128,294 @@ export const ShowHostingerServers = () => { -
- {vpsPlans - ?.sort((a: any, b: any) => { - // Sort by monthly promotional price (first_period_price) - const monthlyPriceA = - a.prices?.find( - (p: any) => p.period === 1 && p.period_unit === "month", - )?.first_period_price || 0; - const monthlyPriceB = - b.prices?.find( - (p: any) => p.period === 1 && p.period_unit === "month", - )?.first_period_price || 0; - return monthlyPriceA - monthlyPriceB; - }) - ?.map((plan: any) => { - const monthlyPrice = - plan.prices?.find( - (p: any) => p.period === 1 && p.period_unit === "month", - )?.first_period_price || 0; - return ( - - {plan.name === "KVM 2" && ( -
- MOST POPULAR +
+ + {/* Data Center Selection */} + ( + + +
+ + + Select Data Center +
- )} - - {plan.name} - - {"High-performance VPS hosting"} - - - -
-
-
- - - {plan.metadata?.cpu || 1} vCPU - - - Cores - -
-
- - - {plan.metadata?.ram || 2} GB - - - RAM - -
-
- - - {plan.metadata?.disk || 20} GB - - - SSD - -
-
- - {plan.prices?.map((price: any) => ( -
-
-
- - - {formatBillingPeriod( - price.period || 1, - price.period_unit || "month", - )} - -
-
- - - ${(price.first_period_price || 0).toFixed(2)} - - {price.period_unit === "year" && ( - - Save $ - {calculateSavings( - monthlyPrice, - price.first_period_price || 0, - ).toFixed(2)} - /yr - - )} -
-
-
+ + + + {dataCenters?.map((center) => ( + + + + + + + + {center.city} / {center.continent} + + + ))} -
-
- - ); - })} -
+ + + + + )} + /> - {(!vpsPlans || vpsPlans.length === 0) && ( -
- Could not load VPS plans. Please retry later. -
- )} + {/* Billing Period Selection */} + ( + + +
+ + + Billing Period + +
+
+ + { + switch (value) { + case "monthly": + field.onChange({ unit: "month", period: 1 }); + break; + case "yearly": + field.onChange({ unit: "year", period: 1 }); + break; + case "2years": + field.onChange({ unit: "year", period: 2 }); + break; + } + }} + defaultValue="monthly" + className="grid w-full grid-cols-3 lg:w-[600px] gap-4" + > + + + + + + Monthly Billing + + + + + + + + Annual Billing + + + + + + + + 2 Year Billing + + + + + +
+ )} + /> + + {/* VPS Plans Selection */} + ( + + +
+ + + Select Server Plan + +
+
+ + + {vpsPlans + ?.sort((a, b) => { + const billingPeriod = form.watch("billingPeriod"); + const priceA = + a.prices?.find( + (p) => + p.period_unit === billingPeriod.unit && + p.period === billingPeriod.period, + )?.price || 0; + const priceB = + b.prices?.find( + (p) => + p.period_unit === billingPeriod.unit && + p.period === billingPeriod.period, + )?.price || 0; + return priceA - priceB; + }) + ?.map((plan) => { + const monthlyPrice = + plan.prices?.find( + (p) => + p.period === 1 && p.period_unit === "month", + )?.price || 0; + + const selectedPrice = plan.prices?.find( + (p) => + p.period_unit === + form.watch("billingPeriod.unit") && + p.period === form.watch("billingPeriod.period"), + ); + + if (!selectedPrice) return null; + + return ( + + + + + + + {plan.name === "KVM 2" && ( +
+ MOST POPULAR +
+ )} + + {plan.name} + + {"High-performance VPS hosting"} + + + +
+
+
+ + + {plan.metadata?.cpus || 1} vCPU + + + Cores + +
+
+ + + {Number.parseInt( + plan.metadata?.memory || "2048", + ) / 1024}{" "} + GB + + + RAM + +
+
+ + + {Number.parseInt( + plan.metadata?.disk_space || + "20480", + ) / 1024}{" "} + GB + + + SSD + +
+
+ +
+
+
+ + + {formatBillingPeriod( + selectedPrice.period || 1, + selectedPrice.period_unit || + "month", + )} + +
+
+ + + $ + {formatPrice( + selectedPrice.price || 0, + )} + + {selectedPrice.period_unit === + "year" && ( + + Save $ + {calculateSavings( + monthlyPrice, + selectedPrice.price || 0, + ).toFixed(2)} + /yr + + )} +
+
+
+
+
+
+
+
+ ); + })} +
+
+ +
+ )} + /> + + + +