Compare commits

...

9 Commits

15 changed files with 37809 additions and 159 deletions
@@ -24,6 +24,7 @@ import { Progress } from "@/components/ui/progress";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { ShowProviders } from "./show-providers";
const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
@@ -151,44 +152,25 @@ export const ShowBilling = () => {
<Loader2 className="animate-spin" />
</span>
) : (
<>
{products?.map((product) => {
const featured = true;
return (
<div key={product.id}>
<section
className={clsx(
"flex flex-col rounded-3xl border-dashed border-2 px-4 max-w-sm",
featured
? "order-first border py-8 lg:order-none"
: "lg:py-8",
)}
>
{isAnnual && (
<div className="mb-4 flex flex-row items-center gap-2">
<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>
) : (
products?.map((product) => {
const featured = true;
return (
<div key={product.id}>
<section
className={clsx(
"flex flex-col rounded-3xl border-dashed border-2 px-4 max-w-sm",
featured
? "order-first border py-8 lg:order-none"
: "lg:py-8",
)}
>
{isAnnual && (
<div className="mb-4 flex flex-row items-center gap-2">
<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(
@@ -196,127 +178,146 @@ export const ShowBilling = () => {
)}{" "}
USD
</p>
)}
<h3 className="mt-5 font-medium text-lg text-primary">
{product.name}
</h3>
<p
className={clsx(
"text-sm",
featured ? "text-white" : "text-slate-400",
)}
>
{product.description}
|
<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-primary">
{product.name}
</h3>
<p
className={clsx(
"text-sm",
featured ? "text-white" : "text-slate-400",
)}
>
{product.description}
</p>
<ul
className={clsx(
" mt-4 flex flex-col gap-y-2 text-sm",
featured ? "text-white" : "text-slate-200",
<ul
className={clsx(
" mt-4 flex flex-col gap-y-2 text-sm",
featured ? "text-white" : "text-slate-200",
)}
>
{[
"All the features of Dokploy",
"Unlimited deployments",
"Self-hosted on your own infrastructure",
"Full access to all deployment features",
"Dokploy integration",
"Backups",
"All Incoming features",
].map((feature) => (
<li
key={feature}
className="flex text-muted-foreground"
>
<CheckIcon />
<span className="ml-4">{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
</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(
data?.subscriptions &&
data?.subscriptions?.length > 0
? "justify-between"
: "justify-end",
"flex flex-row items-center gap-2 mt-4",
)}
>
{[
"All the features of Dokploy",
"Unlimited deployments",
"Self-hosted on your own infrastructure",
"Full access to all deployment features",
"Dokploy integration",
"Backups",
"All Incoming features",
].map((feature) => (
<li
key={feature}
className="flex text-muted-foreground"
>
<CheckIcon />
<span className="ml-4">{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
</span>
</div>
<div className="flex items-center space-x-2">
{admin?.user.stripeCustomerId && (
<Button
disabled={serverQuantity <= 1}
variant="outline"
onClick={() => {
if (serverQuantity <= 1) return;
variant="secondary"
className="w-full"
onClick={async () => {
const session =
await createCustomerPortalSession();
setServerQuantity(serverQuantity - 1);
window.open(session.url);
}}
>
<MinusIcon className="h-4 w-4" />
Manage Subscription
</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(
data?.subscriptions &&
data?.subscriptions?.length > 0
? "justify-between"
: "justify-end",
"flex flex-row items-center gap-2 mt-4",
)}
>
{admin?.user.stripeCustomerId && (
{data?.subscriptions?.length === 0 && (
<div className="justify-end w-full">
<Button
variant="secondary"
className="w-full"
onClick={async () => {
const session =
await createCustomerPortalSession();
window.open(session.url);
handleCheckout(product.id);
}}
disabled={serverQuantity < 1}
>
Manage Subscription
Subscribe
</Button>
)}
{data?.subscriptions?.length === 0 && (
<div className="justify-end w-full">
<Button
className="w-full"
onClick={async () => {
handleCheckout(product.id);
}}
disabled={serverQuantity < 1}
>
Subscribe
</Button>
</div>
)}
</div>
</div>
)}
</div>
</section>
</div>
);
})}
</>
</div>
</section>
</div>
);
})
)}
</div>
</CardContent>
<div className="flex flex-col gap-4 pb-10">
<ShowProviders />
</div>
</div>
</Card>
</div>
@@ -0,0 +1,355 @@
import { zodResolver } from "@hookform/resolvers/zod";
import {
Cpu,
EuroIcon,
HardDrive,
Loader2,
MapPin,
MemoryStick,
Zap,
} 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,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from "@/components/ui/form";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
// Function to classify servers by type
function getServerCategory(cpuType: string) {
if (cpuType === "shared") {
return {
category: "Shared CPU",
icon: Cpu,
badge: "shared",
color: "bg-blue-500",
description: "Perfect for small and medium projects",
};
}
return {
category: "Dedicated CPU",
icon: Zap,
badge: "dedicated",
color: "bg-purple-500",
description: "Maximum performance for demanding applications",
};
}
const formSchema = z.object({
location: z.string().min(1, "Please select a location"),
architecture: z.enum(["x86", "arm"], {
required_error: "Please select an architecture",
}),
selectedServerId: z.string().optional(),
});
type FormValues = z.infer<typeof formSchema>;
export const ShowHetznerProviders = () => {
const { data: serverTypesData, isLoading: isLoadingTypes } =
api.hetzner.serverTypes.useQuery();
const { data: locationsData, isLoading: isLoadingLocations } =
api.hetzner.locations.useQuery();
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
location: "",
architecture: "x86",
selectedServerId: "",
},
});
const selectedLocation = form.watch("location");
const selectedArchitecture = form.watch("architecture");
if (isLoadingTypes || isLoadingLocations) {
return (
<div className="flex items-center justify-center p-4">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
);
}
const locations = locationsData?.locations ?? [];
const serverTypes = serverTypesData?.server_types ?? [];
// Filter server types by selected location AND architecture
const filteredServerTypes = serverTypes.filter(
(type) =>
type.prices.some((price) => price.location === selectedLocation) &&
type.architecture === selectedArchitecture,
);
// Group by CPU type (shared/dedicated)
const sharedServers = filteredServerTypes.filter(
(type) => type.cpu_type === "shared",
);
const dedicatedServers = filteredServerTypes.filter(
(type) => type.cpu_type === "dedicated",
);
const renderServerGrid = (
servers: typeof serverTypes,
category: ReturnType<typeof getServerCategory>,
) => {
if (!servers.length) return null;
const IconComponent = category.icon;
return (
<div className="space-y-4">
<div className="flex items-center gap-2">
<IconComponent className="h-5 w-5" />
<h3 className="text-lg font-semibold">{category.category}</h3>
<Badge variant="outline" className={`text-white ${category.color}`}>
{category.badge}
</Badge>
<Badge variant="outline" className="text-xs text-muted-foreground">
Sorted by price
</Badge>
</div>
<p className="text-sm text-muted-foreground mb-4">
{category.description}
</p>
<FormField
control={form.control}
name="selectedServerId"
render={({ field }) => (
<FormItem className="space-y-0">
<FormControl>
<RadioGroup
onValueChange={field.onChange}
value={field.value}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
>
{servers.map((server) => (
<div key={server.id} className="relative">
<RadioGroupItem
value={server.id.toString()}
id={`server-${server.id}`}
className="absolute right-4 top-4 z-10"
/>
<label htmlFor={`server-${server.id}`}>
<Card
className={`relative bg-transparent transition-all duration-200 cursor-pointer ${
field.value === server.id.toString()
? "border-primary bg-primary/5"
: "hover:bg-primary/5"
}`}
>
<CardHeader>
<CardTitle>{server.name}</CardTitle>
<CardDescription>
{server.description}
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<Cpu className="h-4 w-4 text-blue-500" />
<div>
<strong>Cores:</strong> {server.cores}
</div>
</div>
<div className="flex items-center gap-2">
<MemoryStick className="h-4 w-4 text-green-500" />
<div>
<strong>Memory:</strong> {server.memory} GB
</div>
</div>
<div className="flex items-center gap-2">
<HardDrive className="h-4 w-4 text-purple-500" />
<div>
<strong>Disk:</strong> {server.disk} GB
</div>
</div>
{/* Show price for selected location */}
{server.prices
.filter((p) => p.location === selectedLocation)
.map((p) => (
<div
key={p.location}
className="flex items-center gap-2"
>
<EuroIcon className="h-4 w-4 text-yellow-500" />
<div>
<strong>Price (monthly):</strong>
{Number.parseFloat(
p.price_monthly.net,
).toFixed(2)}
</div>
</div>
))}
</div>
</CardContent>
</Card>
</label>
</div>
))}
</RadioGroup>
</FormControl>
</FormItem>
)}
/>
</div>
);
};
function onSubmit(values: FormValues) {
console.log("Form submitted:", values);
// Here you can handle the form submission with the selected server
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* Filters Card */}
<Card className="bg-transparent">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xl">
<MapPin className="h-5 w-5" />
Filters
</CardTitle>
<CardDescription>
Choose a region and architecture to see location-specific pricing
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<Form {...form}>
<div className="space-y-6">
{/* Region Selector */}
<FormField
control={form.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel>Region</FormLabel>
<Select
value={field.value}
onValueChange={field.onChange}
>
<SelectTrigger>
<SelectValue placeholder="Select a region" />
</SelectTrigger>
<SelectContent>
{locations.map((loc) => (
<SelectItem key={loc.id} value={loc.name}>
{loc.description}
</SelectItem>
))}
</SelectContent>
</Select>
</FormItem>
)}
/>
{/* Architecture Selector */}
<FormField
control={form.control}
name="architecture"
render={({ field }) => (
<FormItem>
<FormLabel>Architecture</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="grid grid-cols-2 gap-4"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="x86" />
</FormControl>
<FormLabel className="font-normal">
x86 (Intel/AMD)
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="arm" />
</FormControl>
<FormLabel className="font-normal">ARM</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
</FormItem>
)}
/>
</div>
</Form>
</CardContent>
</Card>
{/* Architecture Information */}
<div className="grid md:grid-cols-2 gap-4">
<div className="p-4 bg-blue-900 rounded-lg border border-blue-600">
<div className="flex items-start gap-2">
<Cpu className="h-5 w-5 text-blue-600 mt-0.5" />
<div className="text-sm text-blue-200">
<strong>x86 Architecture:</strong> Traditional Intel/AMD
processors. Most compatible with existing software and
applications. Best choice for general-purpose workloads.
</div>
</div>
</div>
<div className="p-4 bg-green-900 rounded-lg border border-green-600">
<div className="flex items-start gap-2">
<Cpu className="h-5 w-5 text-green-600 mt-0.5" />
<div className="text-sm text-green-200">
<strong>ARM Architecture:</strong> Modern, energy-efficient
processors. Excellent price-to-performance ratio. Perfect for
cloud-native and containerized applications.
</div>
</div>
</div>
</div>
{/* Server Types Grid */}
{selectedLocation && (
<>
{renderServerGrid(sharedServers, getServerCategory("shared"))}
{renderServerGrid(dedicatedServers, getServerCategory("dedicated"))}
{sharedServers.length === 0 && dedicatedServers.length === 0 && (
<p className="text-center text-muted-foreground py-10">
No server types available for this region and architecture
combination. <br />
Please try a different region or architecture.
</p>
)}
</>
)}
{selectedLocation && form.watch("selectedServerId") && (
<div className="flex justify-end">
<Button type="submit" className="bg-primary">
Continue with Selected Server
</Button>
</div>
)}
</form>
</Form>
);
};
@@ -0,0 +1,423 @@
import { zodResolver } from "@hookform/resolvers/zod";
import {
Calendar,
Cpu,
DollarSign,
Globe,
HardDrive,
Loader2,
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,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
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") {
return period === 1 ? "Monthly" : `${period} months`;
}
if (unit === "year") {
return period === 1 ? "Yearly" : `${period} years`;
}
return `${period} ${unit}`;
}
// 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<typeof formSchema>;
export const ShowHostingerServers = () => {
const { data: vpsPlans, isLoading: plansLoading } =
api.hostinger.vpsPlans.useQuery();
const { data: dataCenters, isLoading: centersLoading } =
api.hostinger.dataCenters.useQuery();
const form = useForm<FormData>({
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 (
<div className="flex items-center justify-center p-4">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
);
}
return (
<div className="space-y-4">
<Card className="bg-transparent">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Server className="h-5 w-5" />
Hostinger VPS Plans
<Badge variant="outline" className="text-xs text-muted-foreground">
Sorted by price
</Badge>
</CardTitle>
<CardDescription>
VPS plans with real pricing from Hostinger API
<br />
<span className="text-xs text-orange-600">
💡 Promotional pricing applies to first billing period only
</span>
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
{/* Data Center Selection */}
<FormField
control={form.control}
name="datacenter"
render={({ field }) => (
<FormItem className="space-y-4">
<FormLabel>
<div className="flex items-center gap-2">
<Globe className="h-5 w-5 text-muted-foreground" />
<span className="text-lg font-medium">
Select Data Center
</span>
</div>
</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"
>
{dataCenters?.map((center) => (
<FormItem key={center.id}>
<FormControl>
<RadioGroupItem
value={center.id?.toString() || ""}
className="peer sr-only"
/>
</FormControl>
<FormLabel className="p-4 rounded-lg border-2 transition-all duration-200 flex flex-col items-center gap-2 peer-aria-checked:border-purple-500 peer-aria-checked:bg-purple-50 dark:peer-aria-checked:bg-purple-950 hover:border-purple-300 cursor-pointer">
<Globe className="h-6 w-6 text-muted-foreground" />
<span className="text-sm font-medium text-center">
{center.city} / {center.continent}
</span>
</FormLabel>
</FormItem>
))}
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Billing Period Selection */}
<FormField
control={form.control}
name="billingPeriod"
render={({ field }) => (
<FormItem className="space-y-4">
<FormLabel>
<div className="flex items-center gap-2">
<Calendar className="h-5 w-5 text-muted-foreground" />
<span className="text-lg font-medium">
Billing Period
</span>
</div>
</FormLabel>
<FormControl>
<RadioGroup
onValueChange={(value) => {
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"
>
<FormItem>
<FormControl>
<RadioGroupItem
value="monthly"
className="peer sr-only"
/>
</FormControl>
<FormLabel className="flex items-center justify-center p-3 rounded-lg border-2 transition-all duration-200 peer-aria-checked:border-purple-500 peer-aria-checked:bg-purple-50 dark:peer-aria-checked:bg-purple-950 hover:border-purple-300 cursor-pointer">
Monthly Billing
</FormLabel>
</FormItem>
<FormItem>
<FormControl>
<RadioGroupItem
value="yearly"
className="peer sr-only"
/>
</FormControl>
<FormLabel className="flex items-center justify-center p-3 rounded-lg border-2 transition-all duration-200 peer-aria-checked:border-purple-500 peer-aria-checked:bg-purple-50 dark:peer-aria-checked:bg-purple-950 hover:border-purple-300 cursor-pointer">
Annual Billing
</FormLabel>
</FormItem>
<FormItem>
<FormControl>
<RadioGroupItem
value="2years"
className="peer sr-only"
/>
</FormControl>
<FormLabel className="flex items-center justify-center p-3 rounded-lg border-2 transition-all duration-200 peer-aria-checked:border-purple-500 peer-aria-checked:bg-purple-50 dark:peer-aria-checked:bg-purple-950 hover:border-purple-300 cursor-pointer">
2 Year Billing
</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* VPS Plans Selection */}
<FormField
control={form.control}
name="plan"
render={({ field }) => (
<FormItem className="space-y-4">
<FormLabel>
<div className="flex items-center gap-2">
<Server className="h-5 w-5 text-muted-foreground" />
<span className="text-lg font-medium">
Select Server Plan
</span>
</div>
</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="grid grid-cols-1 lg:grid-cols-2 gap-6"
>
{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 (
<FormItem key={plan.id}>
<FormControl>
<RadioGroupItem
value={plan.id || ""}
className="peer sr-only"
/>
</FormControl>
<FormLabel className="w-full cursor-pointer">
<Card
className={`border-2 transition-all duration-200 relative bg-transparent hover:border-purple-300 hover:shadow-lg ${field.value === plan.id ? "border-purple-500 bg-purple-950/40" : ""}`}
>
{plan.name === "KVM 2" && (
<div className="absolute -top-2 -right-2 bg-green-500 text-white px-2 py-1 rounded-full text-xs font-semibold">
MOST POPULAR
</div>
)}
<CardHeader>
<CardTitle>{plan.name}</CardTitle>
<CardDescription>
{"High-performance VPS hosting"}
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-4">
<div className="grid grid-cols-3 gap-4">
<div className="flex flex-col items-center p-2 bg-muted rounded-lg">
<Cpu className="h-4 w-4 mb-1 text-muted-foreground" />
<span className="text-sm font-medium">
{plan.metadata?.cpus || 1} vCPU
</span>
<span className="text-xs text-muted-foreground">
Cores
</span>
</div>
<div className="flex flex-col items-center p-2 bg-muted rounded-lg">
<MemoryStick className="h-4 w-4 mb-1 text-muted-foreground" />
<span className="text-sm font-medium">
{Number.parseInt(
plan.metadata?.memory || "2048",
) / 1024}{" "}
GB
</span>
<span className="text-xs text-muted-foreground">
RAM
</span>
</div>
<div className="flex flex-col items-center p-2 bg-muted rounded-lg">
<HardDrive className="h-4 w-4 mb-1 text-muted-foreground" />
<span className="text-sm font-medium">
{Number.parseInt(
plan.metadata?.disk_space ||
"20480",
) / 1024}{" "}
GB
</span>
<span className="text-xs text-muted-foreground">
SSD
</span>
</div>
</div>
<div className="mt-2">
<div className="flex items-center justify-between p-3 bg-primary/5 rounded-lg">
<div className="flex items-center">
<Calendar className="h-4 w-4 mr-2 text-muted-foreground" />
<span className="text-sm">
{formatBillingPeriod(
selectedPrice.period || 1,
selectedPrice.period_unit ||
"month",
)}
</span>
</div>
<div className="flex items-center">
<DollarSign className="h-4 w-4 mr-1 text-primary" />
<span className="text-lg font-semibold">
$
{formatPrice(
selectedPrice.price || 0,
)}
</span>
{selectedPrice.period_unit ===
"year" && (
<Badge
variant="outline"
className="ml-2 text-xs"
>
Save $
{calculateSavings(
monthlyPrice,
selectedPrice.price || 0,
).toFixed(2)}
/yr
</Badge>
)}
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</FormLabel>
</FormItem>
);
})}
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Create Server
</Button>
</form>
</Form>
</CardContent>
</Card>
</div>
);
};
@@ -0,0 +1,47 @@
import { DollarSign } from "lucide-react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ShowHetznerProviders } from "./show-hetzner-providers";
import { ShowHostingerServers } from "./show-hostinger-servers";
export const ShowProviders = () => {
return (
<Card className="w-full bg-transparent border-none">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<DollarSign className="h-6 w-6 text-green-600" />
Servers
</CardTitle>
<CardDescription>
Manage and view available server types from Hetzner and Hostinger for
your business. Here you can see updated pricing and specifications for
each plan.
</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="hetzner" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="hetzner" className="flex items-center gap-2">
🇩🇪 Hetzner Cloud
</TabsTrigger>
<TabsTrigger value="hostinger" className="flex items-center gap-2">
🌍 Hostinger VPS
</TabsTrigger>
</TabsList>
<TabsContent value="hetzner" className="mt-4">
<ShowHetznerProviders />
</TabsContent>
<TabsContent value="hostinger" className="mt-4">
<ShowHostingerServers />
</TabsContent>
</Tabs>
</CardContent>
</Card>
);
};
+2 -1
View File
@@ -182,7 +182,8 @@
"tsx": "^4.16.2",
"typescript": "^5.8.3",
"vite-tsconfig-paths": "4.3.2",
"vitest": "^1.6.1"
"vitest": "^1.6.1",
"openapi-typescript": "7.8.0"
},
"ct3aMetadata": {
"initVersion": "7.25.2"
+4
View File
@@ -16,6 +16,8 @@ import { gitProviderRouter } from "./routers/git-provider";
import { giteaRouter } from "./routers/gitea";
import { githubRouter } from "./routers/github";
import { gitlabRouter } from "./routers/gitlab";
import { hetznerRouter } from "./routers/hetzner";
import { hostingerRouter } from "./routers/hostinger";
import { mariadbRouter } from "./routers/mariadb";
import { mongoRouter } from "./routers/mongo";
import { mountRouter } from "./routers/mount";
@@ -85,6 +87,8 @@ export const appRouter = createTRPCRouter({
schedule: scheduleRouter,
rollback: rollbackRouter,
volumeBackups: volumeBackupsRouter,
hetzner: hetznerRouter,
hostinger: hostingerRouter,
environment: environmentRouter,
});
@@ -0,0 +1,21 @@
import {
fetchHetznerLocations,
fetchHetznerServers,
fetchHetznerServerTypes,
} from "@dokploy/server/index";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const hetznerRouter = createTRPCRouter({
locations: protectedProcedure.query(async () => {
const locations = await fetchHetznerLocations();
return locations;
}),
serverTypes: protectedProcedure.query(async () => {
return await fetchHetznerServerTypes();
}),
servers: protectedProcedure.query(async () => {
return await fetchHetznerServers();
}),
});
@@ -0,0 +1,16 @@
import {
fetchHostingerCatalog,
fetchHostingerDataCenters,
} from "@dokploy/server/index";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const hostingerRouter = createTRPCRouter({
vpsPlans: protectedProcedure.query(async () => {
const catalogItems = await fetchHostingerCatalog();
return catalogItems.filter((item) => item?.name?.startsWith("KVM"));
}),
dataCenters: protectedProcedure.query(async () => {
return await fetchHostingerDataCenters();
}),
});
+1
View File
@@ -28,6 +28,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"openapi-fetch": "0.14.0",
"@ai-sdk/anthropic": "^2.0.5",
"@ai-sdk/azure": "^2.0.16",
"@ai-sdk/cohere": "^2.0.4",
+2 -2
View File
@@ -21,6 +21,8 @@ export * from "./services/git-provider";
export * from "./services/gitea";
export * from "./services/github";
export * from "./services/gitlab";
export * from "./services/hetzner";
export * from "./services/hostinger";
export * from "./services/mariadb";
export * from "./services/mongo";
export * from "./services/mount";
@@ -76,7 +78,6 @@ export * from "./utils/builders/nixpacks";
export * from "./utils/builders/paketo";
export * from "./utils/builders/static";
export * from "./utils/builders/utils";
export * from "./utils/cluster/upload";
export * from "./utils/databases/rebuild";
export * from "./utils/docker/collision";
@@ -106,7 +107,6 @@ export * from "./utils/providers/docker";
export * from "./utils/providers/git";
export * from "./utils/providers/gitea";
export * from "./utils/providers/github";
export * from "./utils/providers/github";
export * from "./utils/providers/gitlab";
export * from "./utils/providers/raw";
export * from "./utils/schedules/index";
+49
View File
@@ -0,0 +1,49 @@
import createClient from "openapi-fetch";
import type { paths } from "../types/hetzner-types";
const HETZNER_API_URL = "https://api.hetzner.cloud/v1";
const hetznerApiKey = process.env.HETZNER_API_KEY;
const client = createClient<paths>({ baseUrl: HETZNER_API_URL });
export const fetchHetznerLocations = async () => {
const { data, error } = await client.GET("/locations", {
headers: {
Authorization: `Bearer ${hetznerApiKey}`,
},
});
if (error) {
throw new Error(`Failed to fetch Hetzner locations: ${error}`);
}
return data;
};
export const fetchHetznerServerTypes = async () => {
const { data, error } = await client.GET("/server_types", {
headers: {
Authorization: `Bearer ${hetznerApiKey}`,
},
});
if (error) {
throw new Error(`Failed to fetch Hetzner server types: ${error}`);
}
return data;
};
export const fetchHetznerServers = async () => {
const { data, error } = await client.GET("/servers", {
headers: {
Authorization: `Bearer ${hetznerApiKey}`,
},
});
if (error) {
throw new Error(`Failed to fetch Hetzner servers: ${error}`);
}
return data;
};
+35
View File
@@ -0,0 +1,35 @@
import createClient from "openapi-fetch";
import type { paths } from "../types/hostinger-types";
const HOSTINGER_API_URL = "https://developers.hostinger.com";
const hostingerApiKey = process.env.HOSTINGER_API_KEY;
const client = createClient<paths>({ baseUrl: HOSTINGER_API_URL });
export const fetchHostingerCatalog = async () => {
const { data, error } = await client.GET("/api/billing/v1/catalog", {
headers: {
Authorization: `Bearer ${hostingerApiKey}`,
},
});
if (error) {
throw new Error(`Failed to fetch Hostinger catalog: ${error}`);
}
return data;
};
export const fetchHostingerDataCenters = async () => {
const { data, error } = await client.GET("/api/vps/v1/data-centers", {
headers: {
Authorization: `Bearer ${hostingerApiKey}`,
},
});
if (error) {
throw new Error(`Failed to fetch Hostinger data centers: ${error}`);
}
return data;
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+160 -17
View File
@@ -518,6 +518,9 @@ importers:
memfs:
specifier: ^4.17.2
version: 4.17.2
openapi-typescript:
specifier: 7.8.0
version: 7.8.0(typescript@5.8.3)
tailwindcss:
specifier: ^3.4.17
version: 3.4.17
@@ -708,6 +711,9 @@ importers:
octokit:
specifier: 3.1.2
version: 3.1.2
openapi-fetch:
specifier: 0.14.0
version: 0.14.0
otpauth:
specifier: ^9.4.0
version: 9.4.0
@@ -3570,6 +3576,16 @@ packages:
peerDependencies:
'@redis/client': ^1.0.0
'@redocly/ajv@8.11.2':
resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
'@redocly/config@0.22.2':
resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==}
'@redocly/openapi-core@1.34.3':
resolution: {integrity: sha512-3arRdUp1fNx55itnjKiUhO6t4Mf91TsrTIYINDNLAZPS0TPd5YpiXRctwjel0qqWoOOhjA34cZ3m4dksLDFUYg==}
engines: {node: '>=18.17.0', npm: '>=9.5.0'}
'@rollup/rollup-android-arm-eabi@4.41.1':
resolution: {integrity: sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==}
cpu: [arm]
@@ -4194,6 +4210,10 @@ packages:
ansi-align@3.0.1:
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
ansi-escapes@7.0.0:
resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==}
engines: {node: '>=18'}
@@ -4440,6 +4460,9 @@ packages:
resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
change-case@5.4.4:
resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
character-entities-html4@2.1.0:
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
@@ -4553,6 +4576,9 @@ packages:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
colorette@1.4.0:
resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
@@ -5462,6 +5488,10 @@ packages:
resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==}
engines: {node: '>=12'}
index-to-position@1.1.0:
resolution: {integrity: sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==}
engines: {node: '>=18'}
inflation@2.1.0:
resolution: {integrity: sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==}
engines: {node: '>= 0.8.0'}
@@ -5676,6 +5706,10 @@ packages:
js-file-download@0.4.12:
resolution: {integrity: sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg==}
js-levenshtein@1.1.6:
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
engines: {node: '>=0.10.0'}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -6131,6 +6165,10 @@ packages:
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
minimatch@5.1.6:
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
engines: {node: '>=10'}
minimatch@7.4.6:
resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==}
engines: {node: '>=10'}
@@ -6382,6 +6420,9 @@ packages:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'}
openapi-fetch@0.14.0:
resolution: {integrity: sha512-PshIdm1NgdLvb05zp8LqRQMNSKzIlPkyMxYFxwyHR+UlKD4t2nUjkDhNxeRbhRSEd3x5EUNh2w5sJYwkhOH4fg==}
openapi-path-templating@2.2.1:
resolution: {integrity: sha512-eN14VrDvl/YyGxxrkGOHkVkWEoPyhyeydOUrbvjoz8K5eIGgELASwN1eqFOJ2CTQMGCy2EntOK1KdtJ8ZMekcg==}
engines: {node: '>=12.20.0'}
@@ -6393,6 +6434,15 @@ packages:
openapi-types@12.1.3:
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
openapi-typescript-helpers@0.0.15:
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
openapi-typescript@7.8.0:
resolution: {integrity: sha512-1EeVWmDzi16A+siQlo/SwSGIT7HwaFAVjvMA7/jG5HMLSnrUOzPL7uSTRZZa4v/LCRxHTApHKtNY6glApEoiUQ==}
hasBin: true
peerDependencies:
typescript: ^5.x
otpauth@9.4.0:
resolution: {integrity: sha512-fHIfzIG5RqCkK9cmV8WU+dPQr9/ebR5QOwGZn2JAr1RQF+lmAuLL2YdtdqvmBjNmgJlYk3KZ4a0XokaEhg1Jsw==}
@@ -6441,6 +6491,10 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
parse-json@8.3.0:
resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
engines: {node: '>=18'}
parseley@0.12.1:
resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==}
@@ -6543,6 +6597,10 @@ packages:
resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==}
engines: {node: '>=12'}
pluralize@8.0.0:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
@@ -7292,6 +7350,10 @@ packages:
resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==}
engines: {node: '>=16'}
supports-color@10.0.0:
resolution: {integrity: sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==}
engines: {node: '>=18'}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@@ -7478,6 +7540,10 @@ packages:
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
engines: {node: '>=12.20'}
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
@@ -7550,6 +7616,9 @@ packages:
peerDependencies:
browserslist: '>= 4.21.0'
uri-js-replace@1.0.1:
resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==}
url-parse@1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
@@ -7778,6 +7847,9 @@ packages:
yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yaml-ast-parser@0.0.43:
resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==}
yaml@2.8.0:
resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==}
engines: {node: '>= 14.6'}
@@ -10613,6 +10685,29 @@ snapshots:
dependencies:
'@redis/client': 1.6.0
'@redocly/ajv@8.11.2':
dependencies:
fast-deep-equal: 3.1.3
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
uri-js-replace: 1.0.1
'@redocly/config@0.22.2': {}
'@redocly/openapi-core@1.34.3(supports-color@10.0.0)':
dependencies:
'@redocly/ajv': 8.11.2
'@redocly/config': 0.22.2
colorette: 1.4.0
https-proxy-agent: 7.0.6(supports-color@10.0.0)
js-levenshtein: 1.1.6
js-yaml: 4.1.0
minimatch: 5.1.6
pluralize: 8.0.0
yaml-ast-parser: 0.0.43
transitivePeerDependencies:
- supports-color
'@rollup/rollup-android-arm-eabi@4.41.1':
optional: true
@@ -11449,7 +11544,7 @@ snapshots:
agent-base@6.0.2:
dependencies:
debug: 4.4.1
debug: 4.4.1(supports-color@10.0.0)
transitivePeerDependencies:
- supports-color
@@ -11493,6 +11588,8 @@ snapshots:
dependencies:
string-width: 4.2.3
ansi-colors@4.1.3: {}
ansi-escapes@7.0.0:
dependencies:
environment: 1.1.0
@@ -11768,6 +11865,8 @@ snapshots:
chalk@5.4.1: {}
change-case@5.4.4: {}
character-entities-html4@2.1.0: {}
character-entities-legacy@1.1.4: {}
@@ -11891,6 +11990,8 @@ snapshots:
color-string: 1.9.1
optional: true
colorette@1.4.0: {}
colorette@2.0.20: {}
combined-stream@1.0.8:
@@ -12063,9 +12164,11 @@ snapshots:
dateformat@4.6.3: {}
debug@4.4.1:
debug@4.4.1(supports-color@10.0.0):
dependencies:
ms: 2.1.3
optionalDependencies:
supports-color: 10.0.0
decamelize@1.2.0: {}
@@ -12137,7 +12240,7 @@ snapshots:
docker-modem@5.0.6:
dependencies:
debug: 4.4.1
debug: 4.4.1(supports-color@10.0.0)
readable-stream: 3.6.2
split-ca: 1.0.1
ssh2: 1.15.0
@@ -12275,7 +12378,7 @@ snapshots:
esbuild-register@3.6.0(esbuild@0.19.12):
dependencies:
debug: 4.4.1
debug: 4.4.1(supports-color@10.0.0)
esbuild: 0.19.12
transitivePeerDependencies:
- supports-color
@@ -12522,7 +12625,7 @@ snapshots:
gaxios@6.7.1:
dependencies:
extend: 3.0.2
https-proxy-agent: 7.0.6
https-proxy-agent: 7.0.6(supports-color@10.0.0)
is-stream: 2.0.1
node-fetch: 2.7.0
uuid: 9.0.1
@@ -12542,7 +12645,7 @@ snapshots:
gel@2.1.0:
dependencies:
'@petamoriken/float16': 3.9.2
debug: 4.4.1
debug: 4.4.1(supports-color@10.0.0)
env-paths: 3.0.0
semver: 7.7.2
shell-quote: 1.8.2
@@ -12769,14 +12872,14 @@ snapshots:
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.4.1
debug: 4.4.1(supports-color@10.0.0)
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.6:
https-proxy-agent@7.0.6(supports-color@10.0.0):
dependencies:
agent-base: 7.1.4
debug: 4.4.1
debug: 4.4.1(supports-color@10.0.0)
transitivePeerDependencies:
- supports-color
@@ -12820,6 +12923,8 @@ snapshots:
indent-string@5.0.0: {}
index-to-position@1.1.0: {}
inflation@2.1.0: {}
inflight@1.0.6:
@@ -12851,7 +12956,7 @@ snapshots:
canonicalize: 1.0.8
chalk: 4.1.2
cross-fetch: 4.1.0
debug: 4.4.1
debug: 4.4.1(supports-color@10.0.0)
hash.js: 1.1.7
json-stringify-safe: 5.0.1
ms: 2.1.3
@@ -12883,7 +12988,7 @@ snapshots:
dependencies:
'@ioredis/commands': 1.2.0
cluster-key-slot: 1.1.2
debug: 4.4.1
debug: 4.4.1(supports-color@10.0.0)
denque: 2.1.0
lodash.defaults: 4.2.0
lodash.isarguments: 3.1.0
@@ -13000,6 +13105,8 @@ snapshots:
js-file-download@0.4.12: {}
js-levenshtein@1.1.6: {}
js-tokens@4.0.0: {}
js-tokens@9.0.1: {}
@@ -13199,7 +13306,7 @@ snapshots:
dependencies:
chalk: 5.4.1
commander: 13.1.0
debug: 4.4.1
debug: 4.4.1(supports-color@10.0.0)
execa: 8.0.1
lilconfig: 3.1.3
listr2: 8.3.3
@@ -13550,7 +13657,7 @@ snapshots:
micromark@4.0.2:
dependencies:
'@types/debug': 4.1.12
debug: 4.4.1
debug: 4.4.1(supports-color@10.0.0)
decode-named-character-reference: 1.1.0
devlop: 1.1.0
micromark-core-commonmark: 2.0.3
@@ -13600,6 +13707,10 @@ snapshots:
dependencies:
brace-expansion: 1.1.11
minimatch@5.1.6:
dependencies:
brace-expansion: 2.0.1
minimatch@7.4.6:
dependencies:
brace-expansion: 2.0.1
@@ -13841,6 +13952,10 @@ snapshots:
dependencies:
mimic-function: 5.0.1
openapi-fetch@0.14.0:
dependencies:
openapi-typescript-helpers: 0.0.15
openapi-path-templating@2.2.1:
dependencies:
apg-lite: 1.0.4
@@ -13851,6 +13966,18 @@ snapshots:
openapi-types@12.1.3: {}
openapi-typescript-helpers@0.0.15: {}
openapi-typescript@7.8.0(typescript@5.8.3):
dependencies:
'@redocly/openapi-core': 1.34.3(supports-color@10.0.0)
ansi-colors: 4.1.3
change-case: 5.4.4
parse-json: 8.3.0
supports-color: 10.0.0
typescript: 5.8.3
yargs-parser: 21.1.1
otpauth@9.4.0:
dependencies:
'@noble/hashes': 1.7.1
@@ -13911,6 +14038,12 @@ snapshots:
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
parse-json@8.3.0:
dependencies:
'@babel/code-frame': 7.27.1
index-to-position: 1.1.0
type-fest: 4.41.0
parseley@0.12.1:
dependencies:
leac: 0.6.0
@@ -14015,6 +14148,8 @@ snapshots:
dependencies:
queue-lit: 1.5.2
pluralize@8.0.0: {}
pngjs@5.0.0: {}
postcss-import@15.1.0(postcss@8.5.3):
@@ -14466,7 +14601,7 @@ snapshots:
require-in-the-middle@7.5.2:
dependencies:
debug: 4.4.1
debug: 4.4.1(supports-color@10.0.0)
module-details-from-path: 1.0.4
resolve: 1.22.10
transitivePeerDependencies:
@@ -14809,6 +14944,8 @@ snapshots:
dependencies:
copy-anything: 3.0.5
supports-color@10.0.0: {}
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@@ -15054,6 +15191,8 @@ snapshots:
type-fest@2.19.0: {}
type-fest@4.41.0: {}
type-is@1.6.18:
dependencies:
media-typer: 0.3.0
@@ -15129,6 +15268,8 @@ snapshots:
escalade: 3.2.0
picocolors: 1.1.1
uri-js-replace@1.0.1: {}
url-parse@1.5.10:
dependencies:
querystringify: 2.2.0
@@ -15193,7 +15334,7 @@ snapshots:
vite-node@1.6.1(@types/node@18.19.104):
dependencies:
cac: 6.7.14
debug: 4.4.1
debug: 4.4.1(supports-color@10.0.0)
pathe: 1.1.2
picocolors: 1.1.1
vite: 5.4.19(@types/node@18.19.104)
@@ -15210,7 +15351,7 @@ snapshots:
vite-tsconfig-paths@4.3.2(typescript@5.8.3)(vite@5.4.19(@types/node@18.19.104)):
dependencies:
debug: 4.4.1
debug: 4.4.1(supports-color@10.0.0)
globrex: 0.1.2
tsconfck: 3.1.6(typescript@5.8.3)
optionalDependencies:
@@ -15237,7 +15378,7 @@ snapshots:
'@vitest/utils': 1.6.1
acorn-walk: 8.3.4
chai: 4.5.0
debug: 4.4.1
debug: 4.4.1(supports-color@10.0.0)
execa: 8.0.1
local-pkg: 0.5.1
magic-string: 0.30.17
@@ -15351,6 +15492,8 @@ snapshots:
yallist@4.0.0: {}
yaml-ast-parser@0.0.43: {}
yaml@2.8.0: {}
yargs-parser@18.1.3: