mirror of
https://github.com/Dokploy/website.git
synced 2026-06-15 20:25:25 +02:00
fix: update statistics values for GitHub stars, Docker downloads, and contributors
This commit is contained in:
308
apps/website/app/[locale]/contact/page.tsx
Normal file
308
apps/website/app/[locale]/contact/page.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Container } from "@/components/Container";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { trackGAEvent } from "@/components/analitycs";
|
||||
import AnimatedGridPattern from "@/components/ui/animated-grid-pattern";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ContactFormData {
|
||||
inquiryType: "" | "support" | "sales" | "other";
|
||||
name: string;
|
||||
email: string;
|
||||
company: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export default function ContactPage() {
|
||||
const t = useTranslations("Contact");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const [formData, setFormData] = useState<ContactFormData>({
|
||||
inquiryType: "",
|
||||
name: "",
|
||||
email: "",
|
||||
company: "",
|
||||
message: "",
|
||||
});
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!formData.inquiryType) {
|
||||
newErrors.inquiryType = t("errors.inquiryTypeRequired");
|
||||
}
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = t("errors.nameRequired");
|
||||
}
|
||||
if (!formData.email.trim()) {
|
||||
newErrors.email = t("errors.emailRequired");
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||
newErrors.email = t("errors.emailInvalid");
|
||||
}
|
||||
if (!formData.company.trim()) {
|
||||
newErrors.company = t("errors.companyRequired");
|
||||
}
|
||||
if (!formData.message.trim()) {
|
||||
newErrors.message = t("errors.messageRequired");
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/contact", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Track successful form submission
|
||||
trackGAEvent({
|
||||
action: "Contact Form Submitted",
|
||||
category: "Contact",
|
||||
label: formData.inquiryType,
|
||||
});
|
||||
|
||||
// Reset form and show success
|
||||
setFormData({
|
||||
inquiryType: "",
|
||||
name: "",
|
||||
email: "",
|
||||
company: "",
|
||||
message: "",
|
||||
});
|
||||
setErrors({});
|
||||
setIsSubmitted(true);
|
||||
} else {
|
||||
throw new Error("Failed to submit form");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error submitting form:", error);
|
||||
alert(t("errorMessage"));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (field: keyof ContactFormData, value: any) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
// Clear error when user starts typing
|
||||
if (errors[field]) {
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors[field];
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isSubmitted) {
|
||||
return (
|
||||
<div className="bg-background py-24 sm:py-32">
|
||||
<Container>
|
||||
<div className="mx-auto max-w-2xl text-center">
|
||||
<h1 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
||||
{t("successTitle")}
|
||||
</h1>
|
||||
<p className="mt-6 text-lg leading-8 text-muted-foreground">
|
||||
{t("successMessage")}
|
||||
</p>
|
||||
<div className="mt-10">
|
||||
<Button onClick={() => setIsSubmitted(false)} variant="outline">
|
||||
{t("buttons.sendAnother")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-background py-24 sm:py-32 relative">
|
||||
<AnimatedGridPattern
|
||||
numSquares={30}
|
||||
maxOpacity={0.1}
|
||||
height={40}
|
||||
width={40}
|
||||
duration={3}
|
||||
repeatDelay={1}
|
||||
className={cn(
|
||||
"[mask-image:radial-gradient(800px_circle_at_center,white,transparent)]",
|
||||
"absolute inset-x-0 inset-y-[-30%] h-[200%] skew-y-12",
|
||||
)}
|
||||
/>
|
||||
<Container>
|
||||
<div className="mx-auto max-w-3xl border border-border rounded-lg p-8 bg-black z-10 relative">
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
||||
{t("title")}
|
||||
</h1>
|
||||
<p className="mt-6 text-lg leading-8 text-muted-foreground">
|
||||
{t("description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="mt-16 space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="inquiryType"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
{t("fields.inquiryType.label")}{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Select
|
||||
value={formData.inquiryType}
|
||||
onValueChange={(value) =>
|
||||
handleInputChange(
|
||||
"inquiryType",
|
||||
value as "support" | "sales" | "other",
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="bg-input">
|
||||
<SelectValue
|
||||
placeholder={t("fields.inquiryType.placeholder")}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="support">
|
||||
{t("fields.inquiryType.options.support")}
|
||||
</SelectItem>
|
||||
<SelectItem value="sales">
|
||||
{t("fields.inquiryType.options.sales")}
|
||||
</SelectItem>
|
||||
<SelectItem value="other">
|
||||
{t("fields.inquiryType.options.other")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.inquiryType && (
|
||||
<p className="text-sm text-red-600">{errors.inquiryType}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
{t("fields.name.label")}{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
||||
placeholder={t("fields.name.placeholder")}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-sm text-red-600">{errors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
{t("fields.email.label")}{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange("email", e.target.value)}
|
||||
placeholder={t("fields.email.placeholder")}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-sm text-red-600">{errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="company"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
{t("fields.company.label")}{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="company"
|
||||
type="text"
|
||||
value={formData.company}
|
||||
onChange={(e) => handleInputChange("company", e.target.value)}
|
||||
placeholder={t("fields.company.placeholder")}
|
||||
/>
|
||||
{errors.company && (
|
||||
<p className="text-sm text-red-600">{errors.company}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="message"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
{t("fields.message.label")}{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
value={formData.message}
|
||||
onChange={(e) => handleInputChange("message", e.target.value)}
|
||||
placeholder={t("fields.message.placeholder")}
|
||||
rows={6}
|
||||
className="flex w-full rounded-md bg-input border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-none"
|
||||
/>
|
||||
{errors.message && (
|
||||
<p className="text-sm text-red-600">{errors.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
{isSubmitting ? t("buttons.sending") : t("buttons.send")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
112
apps/website/app/api/contact/route.ts
Normal file
112
apps/website/app/api/contact/route.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { Resend } from "resend";
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
interface ContactFormData {
|
||||
inquiryType: "support" | "sales" | "other";
|
||||
name: string;
|
||||
email: string;
|
||||
company: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body: ContactFormData = await request.json();
|
||||
|
||||
// Validate required fields
|
||||
if (
|
||||
!body.inquiryType ||
|
||||
!body.name ||
|
||||
!body.email ||
|
||||
!body.company ||
|
||||
!body.message
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: "All fields are required" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(body.email)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid email format" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Determine recipient email based on inquiry type
|
||||
|
||||
// Format email content
|
||||
const emailSubject = `[${body.inquiryType.toUpperCase()}] New contact form submission from ${body.name}`;
|
||||
const emailBody = `
|
||||
New contact form submission:
|
||||
|
||||
Type: ${body.inquiryType}
|
||||
Name: ${body.name}
|
||||
Email: ${body.email}
|
||||
Company: ${body.company}
|
||||
|
||||
Message:
|
||||
${body.message}
|
||||
|
||||
---
|
||||
Sent from Dokploy website contact form
|
||||
`.trim();
|
||||
|
||||
// Send email to Dokploy team
|
||||
await resend.emails.send({
|
||||
from: "Dokploy Contact Form <noreply@emails.dokploy.com>",
|
||||
to: ["sales@dokploy.com", "contact@dokploy.com"],
|
||||
subject: emailSubject,
|
||||
text: emailBody,
|
||||
replyTo: body.email,
|
||||
});
|
||||
|
||||
// Send confirmation email to the user
|
||||
const confirmationSubject =
|
||||
"Thank you for contacting Dokploy - We received your message";
|
||||
const confirmationBody = `
|
||||
Hello ${body.name},
|
||||
|
||||
Thank you for reaching out to us! We have successfully received your message and our team will get back to you as soon as possible.
|
||||
|
||||
Here's a summary of what you sent us:
|
||||
|
||||
Subject: ${body.inquiryType.charAt(0).toUpperCase() + body.inquiryType.slice(1)} inquiry
|
||||
Company: ${body.company}
|
||||
Message: ${body.message}
|
||||
|
||||
We typically respond within 24-48 hours during business days. If your inquiry is urgent, please don't hesitate to reach out to us directly.
|
||||
|
||||
Best regards,
|
||||
The Dokploy Team
|
||||
|
||||
---
|
||||
This is an automated confirmation email. Please do not reply to this email.
|
||||
If you need immediate assistance, contact us at contact@dokploy.com
|
||||
`.trim();
|
||||
|
||||
await resend.emails.send({
|
||||
from: "Dokploy Team <noreply@emails.dokploy.com>",
|
||||
to: [body.email],
|
||||
subject: confirmationSubject,
|
||||
text: confirmationBody,
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: "Contact form submitted successfully" },
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error processing contact form:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,7 @@ function MobileNavigation() {
|
||||
{t("navigation.docs")}
|
||||
</MobileNavLink>
|
||||
<MobileNavLink href="/blog">{t("navigation.blog")}</MobileNavLink>
|
||||
<MobileNavLink href="/contact">{t("navigation.contact")}</MobileNavLink>
|
||||
<MobileNavLink href={linkT("docs.intro")} target="_blank">
|
||||
<Button className=" w-full" asChild>
|
||||
<Link
|
||||
@@ -184,6 +185,25 @@ export function Header() {
|
||||
</svg>
|
||||
</Link>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="rounded-full max-md:hidden"
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
href="/contact"
|
||||
onClick={() => {
|
||||
trackGAEvent({
|
||||
action: "Contact Button Clicked",
|
||||
category: "Contact",
|
||||
label: "Header",
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("navigation.contact")}
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
{/* <Link
|
||||
className={buttonVariants({
|
||||
variant: "outline",
|
||||
|
||||
@@ -4,9 +4,9 @@ import { useId } from "react";
|
||||
import NumberTicker from "./ui/number-ticker";
|
||||
|
||||
const statsValues = {
|
||||
githubStars: 23000,
|
||||
dockerDownloads: 2500000,
|
||||
contributors: 194,
|
||||
githubStars: 25000,
|
||||
dockerDownloads: 3500000,
|
||||
contributors: 200,
|
||||
sponsors: 50,
|
||||
};
|
||||
|
||||
|
||||
122
apps/website/components/ui/dialog.tsx
Normal file
122
apps/website/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
};
|
||||
@@ -16,7 +16,8 @@
|
||||
"blog": "Blog",
|
||||
"home": "Home",
|
||||
"login": "Login",
|
||||
"register": "Register"
|
||||
"register": "Register",
|
||||
"contact": "Contact"
|
||||
},
|
||||
"hero": {
|
||||
"cloud": "Introducing Dokploy Cloud",
|
||||
@@ -215,5 +216,52 @@
|
||||
"backToBlog": "Back to Blog",
|
||||
"tags": "Tags",
|
||||
"postsTaggedWith": "Posts tagged with"
|
||||
},
|
||||
"Contact": {
|
||||
"title": "Contact Us",
|
||||
"description": "Get in touch with our team. We're here to help with any questions about Dokploy.",
|
||||
"successTitle": "Thank you for contacting us!",
|
||||
"successMessage": "We've received your message and will get back to you as soon as possible.",
|
||||
"errorMessage": "There was an error sending your message. Please try again.",
|
||||
"fields": {
|
||||
"inquiryType": {
|
||||
"label": "What can we help you with today?",
|
||||
"placeholder": "Select an option",
|
||||
"options": {
|
||||
"support": "Support",
|
||||
"sales": "Sales",
|
||||
"other": "Other"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"label": "Name",
|
||||
"placeholder": "Your full name"
|
||||
},
|
||||
"email": {
|
||||
"label": "Email",
|
||||
"placeholder": "your.email@company.com"
|
||||
},
|
||||
"company": {
|
||||
"label": "Company Name",
|
||||
"placeholder": "Your company name"
|
||||
},
|
||||
"message": {
|
||||
"label": "How can we help?",
|
||||
"placeholder": "Tell us more about your inquiry..."
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"send": "Send Message",
|
||||
"sending": "Sending...",
|
||||
"sendAnother": "Send Another Message"
|
||||
},
|
||||
"errors": {
|
||||
"inquiryTypeRequired": "Please select what we can help you with",
|
||||
"nameRequired": "Name is required",
|
||||
"emailRequired": "Email is required",
|
||||
"emailInvalid": "Please enter a valid email address",
|
||||
"companyRequired": "Company name is required",
|
||||
"messageRequired": "Message is required"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"blog": "Blog",
|
||||
"home": "Inicio",
|
||||
"login": "Iniciar sesión",
|
||||
"register": "Registro"
|
||||
"register": "Registro",
|
||||
"contact": "Contacto"
|
||||
},
|
||||
"hero": {
|
||||
"cloud": "Introduciendo Dokploy Cloud",
|
||||
@@ -215,5 +216,52 @@
|
||||
"backToBlog": "Volver al Blog",
|
||||
"tags": "Etiquetas",
|
||||
"postsTaggedWith": "Publicaciones etiquetadas con"
|
||||
},
|
||||
"Contact": {
|
||||
"title": "Contáctanos",
|
||||
"description": "Ponte en contacto con nuestro equipo. Estamos aquí para ayudarte con cualquier pregunta sobre Dokploy.",
|
||||
"successTitle": "¡Gracias por contactarnos!",
|
||||
"successMessage": "Hemos recibido tu mensaje y te responderemos lo antes posible.",
|
||||
"errorMessage": "Hubo un error al enviar tu mensaje. Por favor, inténtalo de nuevo.",
|
||||
"fields": {
|
||||
"inquiryType": {
|
||||
"label": "¿En qué podemos ayudarte hoy?",
|
||||
"placeholder": "Selecciona una opción",
|
||||
"options": {
|
||||
"support": "Soporte",
|
||||
"sales": "Ventas",
|
||||
"other": "Otro"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"label": "Nombre",
|
||||
"placeholder": "Tu nombre completo"
|
||||
},
|
||||
"email": {
|
||||
"label": "Correo electrónico",
|
||||
"placeholder": "tu.email@empresa.com"
|
||||
},
|
||||
"company": {
|
||||
"label": "Nombre de la empresa",
|
||||
"placeholder": "El nombre de tu empresa"
|
||||
},
|
||||
"message": {
|
||||
"label": "¿Cómo podemos ayudarte?",
|
||||
"placeholder": "Cuéntanos más sobre tu consulta..."
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"send": "Enviar mensaje",
|
||||
"sending": "Enviando...",
|
||||
"sendAnother": "Enviar otro mensaje"
|
||||
},
|
||||
"errors": {
|
||||
"inquiryTypeRequired": "Por favor selecciona en qué podemos ayudarte",
|
||||
"nameRequired": "El nombre es obligatorio",
|
||||
"emailRequired": "El correo electrónico es obligatorio",
|
||||
"emailInvalid": "Por favor ingresa un correo electrónico válido",
|
||||
"companyRequired": "El nombre de la empresa es obligatorio",
|
||||
"messageRequired": "El mensaje es obligatorio"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"blog": "Blog",
|
||||
"home": "Accueil",
|
||||
"login": "Connexion",
|
||||
"register": "S'inscrire"
|
||||
"register": "S'inscrire",
|
||||
"contact": "Contact"
|
||||
},
|
||||
"hero": {
|
||||
"cloud": "Présentation de Dokploy Cloud",
|
||||
@@ -209,5 +210,52 @@
|
||||
"allTags": "Tous les tags",
|
||||
"relatedPosts": "Articles similaires",
|
||||
"tagDescription": "Articles tagués avec"
|
||||
},
|
||||
"Contact": {
|
||||
"title": "Nous contacter",
|
||||
"description": "Contactez notre équipe. Nous sommes là pour vous aider avec toutes vos questions sur Dokploy.",
|
||||
"successTitle": "Merci de nous avoir contactés !",
|
||||
"successMessage": "Nous avons reçu votre message et vous répondrons dans les plus brefs délais.",
|
||||
"errorMessage": "Il y a eu une erreur lors de l'envoi de votre message. Veuillez réessayer.",
|
||||
"fields": {
|
||||
"inquiryType": {
|
||||
"label": "Comment pouvons-nous vous aider aujourd'hui ?",
|
||||
"placeholder": "Sélectionnez une option",
|
||||
"options": {
|
||||
"support": "Support",
|
||||
"sales": "Ventes",
|
||||
"other": "Autre"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"label": "Nom",
|
||||
"placeholder": "Votre nom complet"
|
||||
},
|
||||
"email": {
|
||||
"label": "Email",
|
||||
"placeholder": "votre.email@entreprise.com"
|
||||
},
|
||||
"company": {
|
||||
"label": "Nom de l'entreprise",
|
||||
"placeholder": "Le nom de votre entreprise"
|
||||
},
|
||||
"message": {
|
||||
"label": "Comment pouvons-nous vous aider ?",
|
||||
"placeholder": "Parlez-nous de votre demande..."
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"send": "Envoyer le message",
|
||||
"sending": "Envoi en cours...",
|
||||
"sendAnother": "Envoyer un autre message"
|
||||
},
|
||||
"errors": {
|
||||
"inquiryTypeRequired": "Veuillez sélectionner comment nous pouvons vous aider",
|
||||
"nameRequired": "Le nom est obligatoire",
|
||||
"emailRequired": "L'email est obligatoire",
|
||||
"emailInvalid": "Veuillez saisir une adresse email valide",
|
||||
"companyRequired": "Le nom de l'entreprise est obligatoire",
|
||||
"messageRequired": "Le message est obligatoire"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"blog": "博客",
|
||||
"home": "首页",
|
||||
"login": "登录",
|
||||
"register": "注册"
|
||||
"register": "注册",
|
||||
"contact": "联系我们"
|
||||
},
|
||||
"hero": {
|
||||
"cloud": "隆重介绍 Dokploy 云",
|
||||
@@ -220,5 +221,52 @@
|
||||
"postsTaggedWith": "标签为",
|
||||
"foundPosts": "{count, plural, =0 {未找到文章} other {找到 # 篇文章}}",
|
||||
"tagTitle": "标签为 {tag} 的文章"
|
||||
},
|
||||
"Contact": {
|
||||
"title": "联系我们",
|
||||
"description": "联系我们的团队。我们随时为您解答关于 Dokploy 的任何问题。",
|
||||
"successTitle": "感谢您联系我们!",
|
||||
"successMessage": "我们已收到您的消息,将尽快回复您。",
|
||||
"errorMessage": "发送消息时出现错误,请重试。",
|
||||
"fields": {
|
||||
"inquiryType": {
|
||||
"label": "我们今天可以为您提供什么帮助?",
|
||||
"placeholder": "选择一个选项",
|
||||
"options": {
|
||||
"support": "技术支持",
|
||||
"sales": "销售咨询",
|
||||
"other": "其他"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"label": "姓名",
|
||||
"placeholder": "您的全名"
|
||||
},
|
||||
"email": {
|
||||
"label": "邮箱",
|
||||
"placeholder": "your.email@company.com"
|
||||
},
|
||||
"company": {
|
||||
"label": "公司名称",
|
||||
"placeholder": "您的公司名称"
|
||||
},
|
||||
"message": {
|
||||
"label": "我们如何帮助您?",
|
||||
"placeholder": "请详细描述您的咨询..."
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"send": "发送消息",
|
||||
"sending": "发送中...",
|
||||
"sendAnother": "发送另一条消息"
|
||||
},
|
||||
"errors": {
|
||||
"inquiryTypeRequired": "请选择我们可以为您提供的帮助",
|
||||
"nameRequired": "姓名为必填项",
|
||||
"emailRequired": "邮箱为必填项",
|
||||
"emailInvalid": "请输入有效的邮箱地址",
|
||||
"companyRequired": "公司名称为必填项",
|
||||
"messageRequired": "消息为必填项"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"@types/turndown": "^5.0.5",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"axios": "^1.8.1",
|
||||
"resend": "6.1.2",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"framer-motion": "^11.3.19",
|
||||
|
||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -193,6 +193,9 @@ importers:
|
||||
remark-toc:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0
|
||||
resend:
|
||||
specifier: 6.1.2
|
||||
version: 6.1.2
|
||||
satori:
|
||||
specifier: ^0.12.1
|
||||
version: 0.12.1
|
||||
@@ -3534,6 +3537,15 @@ packages:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
resend@6.1.2:
|
||||
resolution: {integrity: sha512-C9Q+YkRe57P8MQlkHG3yatSR/B6sqBGA06Ri2DveJfkz9Vm16182FC/iHB0K6IAfmqZ4yRrFebFw1EPuktLtSg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@react-email/render': '*'
|
||||
peerDependenciesMeta:
|
||||
'@react-email/render':
|
||||
optional: true
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -7806,6 +7818,8 @@ snapshots:
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
resend@6.1.2: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
resolve-from@5.0.0: {}
|
||||
|
||||
Reference in New Issue
Block a user