refactor: standardize code formatting and improve readability

- Updated various files to ensure consistent code formatting, including adjusting indentation and spacing.
- Refactored components and utility functions for better readability and maintainability.
- Removed unnecessary newlines and ensured consistent use of single quotes for strings across the codebase.
This commit is contained in:
Mauricio Siu
2025-11-30 01:46:48 -06:00
parent 878f7b82bf
commit af1b2dbd7a
85 changed files with 3279 additions and 2868 deletions

View File

@@ -42,7 +42,6 @@ Example:
feat: add new feature
```
## Pull Request
- The `main` branch is the source of truth and should always reflect the latest stable release.

View File

@@ -4,114 +4,114 @@ import { baseUrl } from "@/utils/metadata";
import { ImageZoom } from "fumadocs-ui/components/image-zoom";
import defaultMdxComponents from "fumadocs-ui/mdx";
import {
DocsBody,
DocsDescription,
DocsPage,
DocsTitle,
DocsBody,
DocsDescription,
DocsPage,
DocsTitle,
} from "fumadocs-ui/page";
import { notFound, permanentRedirect } from "next/navigation";
export default async function Page(props: {
params: Promise<{ slug?: string[] }>;
params: Promise<{ slug?: string[] }>;
}) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) {
permanentRedirect("/docs/core");
}
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) {
permanentRedirect("/docs/core");
}
const MDX = page.data.body;
const MDX = page.data.body;
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
<MDX
components={{
...defaultMdxComponents,
ImageZoom: (props) => <ImageZoom {...(props as any)} />,
p: ({ children }) => (
<p className="text-[#3E4342] dark:text-muted-foreground">
{children}
</p>
),
li: ({ children, id }) => (
<li
{...{ id }}
className="text-[#3E4342] dark:text-muted-foreground"
>
{children}
</li>
),
APIPage: openapi.APIPage,
}}
/>
</DocsBody>
</DocsPage>
);
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
<MDX
components={{
...defaultMdxComponents,
ImageZoom: (props) => <ImageZoom {...(props as any)} />,
p: ({ children }) => (
<p className="text-[#3E4342] dark:text-muted-foreground">
{children}
</p>
),
li: ({ children, id }) => (
<li
{...{ id }}
className="text-[#3E4342] dark:text-muted-foreground"
>
{children}
</li>
),
APIPage: openapi.APIPage,
}}
/>
</DocsBody>
</DocsPage>
);
}
export async function generateStaticParams() {
return source.generateParams();
return source.generateParams();
}
export async function generateMetadata(props: {
params: Promise<{ slug?: string[] }>;
params: Promise<{ slug?: string[] }>;
}) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
return {
title: page.data.title,
return {
title: page.data.title,
description: page.data.description,
robots: "index,follow",
alternates: {
canonical: new URL(`${baseUrl}${page.url}`).toString(),
languages: {
en: `${baseUrl}/${page.url}`,
},
},
openGraph: {
title: page.data.title,
description: page.data.description,
url: new URL(`${baseUrl}`).toString(),
images: [
{
url: new URL(`${baseUrl}/logo.png`).toString(),
width: 1200,
height: 630,
alt: page.data.title,
},
],
},
twitter: {
card: "summary_large_image",
creator: "@getdokploy",
title: page.data.title,
description: page.data.description,
images: [
{
url: new URL(`${baseUrl}/logo.png`).toString(),
width: 1200,
height: 630,
alt: page.data.title,
},
],
},
applicationName: "Dokploy Docs",
keywords: [
"dokploy",
"vps",
"open source",
"cloud",
"self hosting",
"free",
],
icons: {
icon: "/icon.svg",
},
};
description: page.data.description,
robots: "index,follow",
alternates: {
canonical: new URL(`${baseUrl}${page.url}`).toString(),
languages: {
en: `${baseUrl}/${page.url}`,
},
},
openGraph: {
title: page.data.title,
description: page.data.description,
url: new URL(`${baseUrl}`).toString(),
images: [
{
url: new URL(`${baseUrl}/logo.png`).toString(),
width: 1200,
height: 630,
alt: page.data.title,
},
],
},
twitter: {
card: "summary_large_image",
creator: "@getdokploy",
title: page.data.title,
description: page.data.description,
images: [
{
url: new URL(`${baseUrl}/logo.png`).toString(),
width: 1200,
height: 630,
alt: page.data.title,
},
],
},
applicationName: "Dokploy Docs",
keywords: [
"dokploy",
"vps",
"open source",
"cloud",
"self hosting",
"free",
],
icons: {
icon: "/icon.svg",
},
};
}

View File

@@ -5,18 +5,18 @@ import { DocsLayout } from "fumadocs-ui/layouts/docs";
import type { ReactNode } from "react";
export const metadata = createMetadata({
title: {
template: "%s | Dokploy",
default: "Dokploy",
},
description: "The Open Source Alternative to Vercel, Heroku, and Netlify",
metadataBase: new URL(baseUrl),
title: {
template: "%s | Dokploy",
default: "Dokploy",
},
description: "The Open Source Alternative to Vercel, Heroku, and Netlify",
metadataBase: new URL(baseUrl),
});
export default function Layout({ children }: { children: ReactNode }) {
return (
<DocsLayout tree={source.pageTree} {...baseOptions}>
{children}
</DocsLayout>
);
return (
<DocsLayout tree={source.pageTree} {...baseOptions}>
{children}
</DocsLayout>
);
}

View File

@@ -1,11 +1,11 @@
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
import {
Github,
GlobeIcon,
HeartIcon,
Rss,
LogIn,
UserPlus,
Github,
GlobeIcon,
HeartIcon,
Rss,
LogIn,
UserPlus,
} from "lucide-react";
import Link from "next/link";
/**
@@ -17,106 +17,106 @@ import Link from "next/link";
*/
export const Logo = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 559 446"
className="!size-8 lg:!size-10"
>
<path
className="fill-primary stroke-primary"
d="M390 56v12c.1 2.3.5 4 1 6a73 73 0 0 0 12 24c2 2.3 5.7 4 7 7 4 3.4 9.6 6.8 14 9 1.7.6 5.7 1.1 7 2 1.9 1.3 2.9 2.3 0 4v1c-.6 1.8-1.9 3.5-3 5q-3 4-7 7c-4.3 3.2-9.5 6.8-15 7h-1q-2 1.6-5 2h-4c-5.2.7-12.9 2.2-18 0h-6c-1.6 0-3-.8-4-1h-3a17 17 0 0 1-6-2h-1c-2.5-.1-4-1.2-6-2l-4-1c-8.4-2-20.3-6.6-27-12h-1c-4.6-1-9.5-4.3-13.7-6.3s-10.5-3-13.3-6.7h-1c-4-1-8.9-3.5-12-6h-1c-6.8-1.6-13.6-6-20-9-6.5-2.8-14.6-5.7-20-10h-1c-7-1.2-15.4-4-22-6h-97c-5.3 4.3-13.7 4.3-18.7 10.3S90.8 101 88 108c-.4 1.5-.8 2.3-1 4-.2 1.6-.8 4-1 5v51c.2 1.2.8 3.2 1 5 .2 2 .5 3.2 1 5a79 79 0 0 0 6 12c.8.7 1.4 2.2 2 3 1.8 2 4.9 3.4 6 6 9.5 8.3 23.5 10.3 33 18h1c5.1 1.2 12 4.8 16 8h1c4 1 8.9 3.5 12 6h1q4.6 1.2 8 4h1c2 .1 2.6 1.3 4 2 1.6.8 2.7.7 4 2h1q2.5.3 4 2h1c3 .7 6.7 2 9 4h1c4.7.8 13.4 3.1 17 6h1c2.5.1 4 1.3 6 2 1.8.4 3 .8 5 1q3 .4 5 1c1.6-.2 2 0 3 1h1q2.5-.5 4 1h1q2.5-.5 4 1h1c2.2-.2 4.5-.3 6 1h1q4-.4 7 1h45c1.2-.2 3.1-1 5-1h6c1.5-.6 2.9-1.3 5-1h1q1.5-1.4 4-1h1q1.5-1.4 4-1h1c2.4-1.3 5-1.6 8-2l5-1c2-.7 3.6-1.6 6-2 4-.7 7.2-1.7 11-3 2.3-1 4.2-2.5 7-3h1q1.5-1.7 4-2h1c1.9-1.5 3.9-2 6-3q2.9-1.6 6-3a95 95 0 0 0 11-5c4.4-2.8 8.9-6 14-8 0 0 .6.2 1 0 1.8-2.8 7-4.8 10-6 0 0 .6.2 1 0 1.5-2.4 5.3-4 8-5 0 0 .6.2 1 0 1.5-2.4 5.3-4 8-5 0 0 .6.2 1 0 1.3-2 3.8-3.1 6-4 0 0 .6.2 1 0 2-3 7.7-5.6 11-7l5-2c6.3-3.8 11.8-9.6 18-14v-1c0-1.9-.4-4.2 0-6-1-4.5-3.9-5.5-7-8h-1c-1.2 0-2.8-.2-4 0-8.9 1.7-16.5 11.3-25.2 14.8-8.8 3.4-16.9 10.7-25.8 14.2h-1c-10.9 10.6-29.2 16-42.7 23.3S343.7 234.6 328 235h-1q-1.5 1.4-4 1h-1q-1.5 1.4-4 1h-1c-1.5 1.3-3.9 1.2-6 1h-1c-1.7 1.3-4.6 1.2-7 1-1 .2-2.4 1-4 1h-5c-6.6 0-13.4.4-20 0-1.9-.1-2.7.3-4-1h-8c-2.8-.2-5.7-1.3-8-2h-2q-5.7.4-10-2h-1q-4.5 0-8-2h-1a10 10 0 0 1-6-2h-1c-5.9-.2-12-3.8-17-6l-4-1c-1.7-.5-2.8-.7-4-2h-1q-2.5-.2-4-2h-1q-3.4-.9-6-3h-1c-3.5-.8-7.3-2.9-10-5h-1c-1.7 0-2.2-.7-3-2h-1c-11.6-2.7-23.2-11.5-34.2-15.8-11-4.2-25.9-9.2-29.8-21.2h4c16.2 0 32.8-1 49 0 1.7.1 3 .8 4 1 2.1.4 3.4-.5 5 1h1c3.6.1 8.4 1.8 11 4h1a45 45 0 0 1 18 8h1q4.6 1.2 8 4h1c4.2 1 8.3 3.4 12 5q3.4 1.2 7 2c5.7 1.3 13 2.3 18 5h1c3.7-.2 7 1.1 10 2h9c1.6 0 3 .8 4 1h32c2.2-1.6 6-1 9-1h1a63 63 0 0 1 22-4 22 22 0 0 1 8-2c1.7-1.4 3.7-1.6 6-2a81 81 0 0 0 12-3c2.3-1 4.2-2.5 7-3h1q1.5-1.7 4-2h1c1.9-1.5 3.6-2.2 6-3l3-1c4.1-2.3 8.4-5.2 13-7 0 0 .6.2 1 0 1.5-2.4 6.3-5 9-6 0 0 .6.2 1 0 5.3-8.1 17.6-12.5 24.8-20.2C439.9 144 445 133 452 126v-1a12 12 0 0 1 2-5c2.1-2.2 8.9-1 12-1q2 .2 4 0c1-.2 2.3-1.2 4-1h1q2.1-1.5 5-2h1q2.1-1.9 5-3s.6.2 1 0c9-9.3 18-15.4 23-28 1.1-2.8 3.5-6.4 4-9 .2-1 .2-3 0-4-1.5-6-12.3-2.4-15.7 2.3S484.7 80 479 80h-7c-7.8 4.3-19.3 5.7-23 16a37 37 0 0 0-22-24c-1.5-.5-2.5-.7-4-1-2.1-.5-3.6-.2-5-2h-1a22 22 0 0 1-12-8c-2-2.9-3.4-6.5-6-9h-1c-3.9-.6-6.1 1-8 4m-181 45h1c2.2-.2 4.5-.3 6 1h1q2.5-.5 4 1h1a33 33 0 0 1 17 7h1c4.4 1 8.2 4.1 12 6 2.1 1 4.1 1.5 6 3h1c4 1 8.9 3.5 12 6h1c4 1 8.9 3.5 12 6h1c4 1 8.9 3.5 12 6h1a61 61 0 0 1 21 10h1c3.5.8 7.3 2.9 10 5h1c6.1 1.4 12.3 5 18 7 1.8.4 3 .8 5 1 1.8.2 3.7.8 5 1q2.5-.5 4 1h6c2.5 0 4 .3 6 1h3q-.7 2.1-3 2a46 46 0 0 1-16 7l-10 3c-2 .8-3.4 1.9-6 2h-1c-2.6 2.1-7.5 3-11 3h-1c-3.1 2.5-10.7 3.5-15 3h-1c-1.5 1.3-3.9 1.2-6 1-1 .2-2.4 1-4 1h-11c-3.8.4-8.3.4-12 0h-9c-2.3 0-4.3-.7-6-1h-3c-1.8 0-2.9-.7-4-1-3.5-.8-7-.7-10-2h-1c-4.1-.7-9.8-1.4-13-4h-1q-4-.6-7-3h-1q-2.5-.2-4-2h-1q-3.4-.9-6-3h-1c-7.2-1.7-13.3-5.9-20.2-8.8-7-2.8-16.2-4.3-22.8-7.2h-11c-14 0-28.9.3-42-1-2.3 0-4.8.3-7 0a6 6 0 0 1-5-5c-1.8-4.8-.4-10.4 0-15 0-4.3-.4-8.7 0-13 .2-3.2 2.2-7.3 4-10q2-3 5-5c2.1-2 5.4-2.3 8-3 15.6-3.9 36.3-1 53-1 5.2 0 12-.5 17 0s12.2-1.8 16 1Z"
/>
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 559 446"
className="!size-8 lg:!size-10"
>
<path
className="fill-primary stroke-primary"
d="M390 56v12c.1 2.3.5 4 1 6a73 73 0 0 0 12 24c2 2.3 5.7 4 7 7 4 3.4 9.6 6.8 14 9 1.7.6 5.7 1.1 7 2 1.9 1.3 2.9 2.3 0 4v1c-.6 1.8-1.9 3.5-3 5q-3 4-7 7c-4.3 3.2-9.5 6.8-15 7h-1q-2 1.6-5 2h-4c-5.2.7-12.9 2.2-18 0h-6c-1.6 0-3-.8-4-1h-3a17 17 0 0 1-6-2h-1c-2.5-.1-4-1.2-6-2l-4-1c-8.4-2-20.3-6.6-27-12h-1c-4.6-1-9.5-4.3-13.7-6.3s-10.5-3-13.3-6.7h-1c-4-1-8.9-3.5-12-6h-1c-6.8-1.6-13.6-6-20-9-6.5-2.8-14.6-5.7-20-10h-1c-7-1.2-15.4-4-22-6h-97c-5.3 4.3-13.7 4.3-18.7 10.3S90.8 101 88 108c-.4 1.5-.8 2.3-1 4-.2 1.6-.8 4-1 5v51c.2 1.2.8 3.2 1 5 .2 2 .5 3.2 1 5a79 79 0 0 0 6 12c.8.7 1.4 2.2 2 3 1.8 2 4.9 3.4 6 6 9.5 8.3 23.5 10.3 33 18h1c5.1 1.2 12 4.8 16 8h1c4 1 8.9 3.5 12 6h1q4.6 1.2 8 4h1c2 .1 2.6 1.3 4 2 1.6.8 2.7.7 4 2h1q2.5.3 4 2h1c3 .7 6.7 2 9 4h1c4.7.8 13.4 3.1 17 6h1c2.5.1 4 1.3 6 2 1.8.4 3 .8 5 1q3 .4 5 1c1.6-.2 2 0 3 1h1q2.5-.5 4 1h1q2.5-.5 4 1h1c2.2-.2 4.5-.3 6 1h1q4-.4 7 1h45c1.2-.2 3.1-1 5-1h6c1.5-.6 2.9-1.3 5-1h1q1.5-1.4 4-1h1q1.5-1.4 4-1h1c2.4-1.3 5-1.6 8-2l5-1c2-.7 3.6-1.6 6-2 4-.7 7.2-1.7 11-3 2.3-1 4.2-2.5 7-3h1q1.5-1.7 4-2h1c1.9-1.5 3.9-2 6-3q2.9-1.6 6-3a95 95 0 0 0 11-5c4.4-2.8 8.9-6 14-8 0 0 .6.2 1 0 1.8-2.8 7-4.8 10-6 0 0 .6.2 1 0 1.5-2.4 5.3-4 8-5 0 0 .6.2 1 0 1.5-2.4 5.3-4 8-5 0 0 .6.2 1 0 1.3-2 3.8-3.1 6-4 0 0 .6.2 1 0 2-3 7.7-5.6 11-7l5-2c6.3-3.8 11.8-9.6 18-14v-1c0-1.9-.4-4.2 0-6-1-4.5-3.9-5.5-7-8h-1c-1.2 0-2.8-.2-4 0-8.9 1.7-16.5 11.3-25.2 14.8-8.8 3.4-16.9 10.7-25.8 14.2h-1c-10.9 10.6-29.2 16-42.7 23.3S343.7 234.6 328 235h-1q-1.5 1.4-4 1h-1q-1.5 1.4-4 1h-1c-1.5 1.3-3.9 1.2-6 1h-1c-1.7 1.3-4.6 1.2-7 1-1 .2-2.4 1-4 1h-5c-6.6 0-13.4.4-20 0-1.9-.1-2.7.3-4-1h-8c-2.8-.2-5.7-1.3-8-2h-2q-5.7.4-10-2h-1q-4.5 0-8-2h-1a10 10 0 0 1-6-2h-1c-5.9-.2-12-3.8-17-6l-4-1c-1.7-.5-2.8-.7-4-2h-1q-2.5-.2-4-2h-1q-3.4-.9-6-3h-1c-3.5-.8-7.3-2.9-10-5h-1c-1.7 0-2.2-.7-3-2h-1c-11.6-2.7-23.2-11.5-34.2-15.8-11-4.2-25.9-9.2-29.8-21.2h4c16.2 0 32.8-1 49 0 1.7.1 3 .8 4 1 2.1.4 3.4-.5 5 1h1c3.6.1 8.4 1.8 11 4h1a45 45 0 0 1 18 8h1q4.6 1.2 8 4h1c4.2 1 8.3 3.4 12 5q3.4 1.2 7 2c5.7 1.3 13 2.3 18 5h1c3.7-.2 7 1.1 10 2h9c1.6 0 3 .8 4 1h32c2.2-1.6 6-1 9-1h1a63 63 0 0 1 22-4 22 22 0 0 1 8-2c1.7-1.4 3.7-1.6 6-2a81 81 0 0 0 12-3c2.3-1 4.2-2.5 7-3h1q1.5-1.7 4-2h1c1.9-1.5 3.6-2.2 6-3l3-1c4.1-2.3 8.4-5.2 13-7 0 0 .6.2 1 0 1.5-2.4 6.3-5 9-6 0 0 .6.2 1 0 5.3-8.1 17.6-12.5 24.8-20.2C439.9 144 445 133 452 126v-1a12 12 0 0 1 2-5c2.1-2.2 8.9-1 12-1q2 .2 4 0c1-.2 2.3-1.2 4-1h1q2.1-1.5 5-2h1q2.1-1.9 5-3s.6.2 1 0c9-9.3 18-15.4 23-28 1.1-2.8 3.5-6.4 4-9 .2-1 .2-3 0-4-1.5-6-12.3-2.4-15.7 2.3S484.7 80 479 80h-7c-7.8 4.3-19.3 5.7-23 16a37 37 0 0 0-22-24c-1.5-.5-2.5-.7-4-1-2.1-.5-3.6-.2-5-2h-1a22 22 0 0 1-12-8c-2-2.9-3.4-6.5-6-9h-1c-3.9-.6-6.1 1-8 4m-181 45h1c2.2-.2 4.5-.3 6 1h1q2.5-.5 4 1h1a33 33 0 0 1 17 7h1c4.4 1 8.2 4.1 12 6 2.1 1 4.1 1.5 6 3h1c4 1 8.9 3.5 12 6h1c4 1 8.9 3.5 12 6h1c4 1 8.9 3.5 12 6h1a61 61 0 0 1 21 10h1c3.5.8 7.3 2.9 10 5h1c6.1 1.4 12.3 5 18 7 1.8.4 3 .8 5 1 1.8.2 3.7.8 5 1q2.5-.5 4 1h6c2.5 0 4 .3 6 1h3q-.7 2.1-3 2a46 46 0 0 1-16 7l-10 3c-2 .8-3.4 1.9-6 2h-1c-2.6 2.1-7.5 3-11 3h-1c-3.1 2.5-10.7 3.5-15 3h-1c-1.5 1.3-3.9 1.2-6 1-1 .2-2.4 1-4 1h-11c-3.8.4-8.3.4-12 0h-9c-2.3 0-4.3-.7-6-1h-3c-1.8 0-2.9-.7-4-1-3.5-.8-7-.7-10-2h-1c-4.1-.7-9.8-1.4-13-4h-1q-4-.6-7-3h-1q-2.5-.2-4-2h-1q-3.4-.9-6-3h-1c-7.2-1.7-13.3-5.9-20.2-8.8-7-2.8-16.2-4.3-22.8-7.2h-11c-14 0-28.9.3-42-1-2.3 0-4.8.3-7 0a6 6 0 0 1-5-5c-1.8-4.8-.4-10.4 0-15 0-4.3-.4-8.7 0-13 .2-3.2 2.2-7.3 4-10q2-3 5-5c2.1-2 5.4-2.3 8-3 15.6-3.9 36.3-1 53-1 5.2 0 12-.5 17 0s12.2-1.8 16 1Z"
/>
<path
className="fill-primary stroke-primary"
d="M162 132v1c1.8 2.9 4.5 5.3 8 6 .3-.2 3.7-.2 4 0 7-1.4 9.2-8.8 7-15v-1a14 14 0 0 0-7-4c-.3.2-3.7.2-4 0-6.5 1.3-8.6 6.8-8 13Z"
/>
<path
className="fill-primary stroke-primary"
d="M162 132v1c1.8 2.9 4.5 5.3 8 6 .3-.2 3.7-.2 4 0 7-1.4 9.2-8.8 7-15v-1a14 14 0 0 0-7-4c-.3.2-3.7.2-4 0-6.5 1.3-8.6 6.8-8 13Z"
/>
<path
className="fill-primary stroke-primary"
d="M465 211h-1c-18.2 14.6-41.2 24.6-60 39-19 14.2-42.7 29.3-66 34l-4 1c-2.4 1-4 2-7 2h-1q-3.5 2-8 2h-1c-1.3 1.2-3 1.1-5 1h-2q-2.6 1.1-6 1h-2c-3 1.2-6.5 1-10 1-6.3.6-13.8.6-20 0-3.4 0-8.4.9-11-1h-1c-2.2.2-4.5.3-6-1h-1c-2 .2-3.7.2-5-1h-1c-7.6.5-16.5-3.4-23-6l-4-1a129 129 0 0 1-36.2-15.8c-10.4-6.6-23.2-12.8-32.5-20.5-9.2-7.7-23.8-12.8-30.3-22.7h-1c-2.3-1.4-4.5-2.7-6-5h-1c-4-2.5-8.5-5.2-12-8h-9a9 9 0 0 0-6 7c.3 3.3 0 6.7 0 10v9c.2 1.6 1 3.8 1 6v3c.2 1 1.2 2.2 1 4v1c1.2 1.2.8 2.2 1 4 .8 6.7 3 12.6 5 19 1.7 4.3 4.2 9.1 5 14v1q1.8 1.5 2 4v1a36 36 0 0 1 5 10c.7 2 1 3 2 5 8 12.7 15.7 25.5 25.8 37.3 10 11.7 20.8 20.6 32.4 30.4 11.7 9.9 28.3 14 39.8 23.3h1q2.5.3 4 2h1c2.8.4 4.8 2 7 3l7 2c5.7 1.3 13 2.3 18 5h1c2.1-.3 3.6.8 5 1h3c2.8.2 5.8 1 8 2h8c2.1 0 4.6.8 6 1h21c1.2-.2 3.2-1 5-1h9c3.3-1 7-2.4 11-2h1c2.7-2.2 7.4-2.4 11-3a55 55 0 0 0 8-2c6.5-2.6 13.9-6.3 21-8h1c8.5-6.8 20.6-9.7 29.2-16.8 8.7-7 18.3-12.8 26.8-20.2 4.4-3.8 9-9 13-13 14.8-14.8 20.7-34.6 33-50v-1q.9-3.4 3-6v-1q.3-2.5 2-4v-1c.5-3.3 2-8.6 4-11v-1q0-3.5 2-6v-1c1.1-6.7 2.4-15 5-21v-1c-.2-2-.2-3.7 1-5v-8c0-5.3-.5-10.8 0-16a14 14 0 0 0-4-6c-1-.5-1.1-.4-2-1h-6q-2.1 1.5-5 2m-6 38c-2.1 13.4-21.2 20.3-31 30-10 9.5-23.7 19-35 27-11.5 8-25.1 19.7-39 23h-1a22 22 0 0 1-10 4h-1a25 25 0 0 1-12 4h-1q-3.5 2-8 2h-1c-1.1 1.1-2.3 1-4 1h-2c-1.2.4-2.2 1-4 1h-2c-1.8.7-3.6 1.3-6 1h-1c-1.2 1.2-2.3 1-4 1h-5c-5.7.6-12.3.8-18 0h-4c-1.9 0-2.7-.6-4-1h-6c-1.9 0-2.7.3-4-1h-1q-2.5.5-4-1h-1c-8.1.5-16.8-3.6-24.2-5.8S210 329.8 204 325h-1c-12.8-5-27.1-15.6-37.7-24.3S138.8 284.2 131 273c-.3-.2-1 0-1 0-5.7-4.4-16.6-10-19-17-.9-2.6-1-5.4-2-8-.8-2.2-2.5-5-2-8a667 667 0 0 0 88 56h1q3.4.9 6 3h1c2.8.4 4.8 2 7 3q5 1.8 10 3l6 2q2.9.6 6 1 3 .4 5 1c1.6-.2 2 0 3 1h1c2-.2 3.7-.2 5 1h1c2.2-.3 3.4.4 5 1h8c1.6 0 3 .9 4 1h40c1.8-1.3 4.6-1.2 7-1h1c1.2-1.2 3.2-1.2 5-1h1c1.2-1.2 3.2-1.2 5-1h1c1.1-1.1 2.3-1 4-1h2c3.5-1.7 6.9-2.3 11-3l4-1c3.4-1.4 7.1-3 11-4 1.5-.4 2.5-.5 4-1 1.4-.7 2-1.9 4-2h1q2.6-2.1 6-3h1c2.5-2 6-3.8 9-5l3-1c1.4-.9 2-2.5 4-3h1q1.4-2.2 4-3h1c7.3-7.7 19-13.2 27.7-19.3 8.8-6.1 18.2-15 28.3-18.7.4-.2 1 0 1 0q3.8-3.9 9-6c1.3 2.5-.5 6.7-1 10m-20 55c-.2.4 0 1 0 1-3.4 9.6-12.7 19-19 27a88 88 0 0 1-12 12 214 214 0 0 1-26.7 20.3c-9.5 5.8-20 14.8-31.3 16.7h-1a22 22 0 0 1-10 4h-1c-3.2 2.6-8.9 3.3-13 4h-1q-1.5 1.4-4 1h-1q-1.5 1.4-4 1h-1c-4.9 2.3-10.5 1-16 2-1 .2-2.5 1-4 1-6.2.4-12.8.3-19 0-1.8 0-3.8-.8-5-1h-4c-1.6 0-3-.9-4-1h-4c-3.9-.3-8.8-1.3-12-3h-1c-3.3-.5-7.5-1-10-3h-1c-3.6-.1-8.4-1.8-11-4h-1c-3.9-.6-8-2.6-11-5h-1c-16.1-3.8-32.2-18.9-45-29a200 200 0 0 1-40-51c17.7 11.5 35 25.5 52 38h1c4 1.6 12.8 5.4 15 9h1c4.6 1 10.4 4.1 14 7h1q2.5.3 4 2h1c3.3.5 8.6 2 11 4h1q3.5 0 6 2h1q2.5-.5 4 1h1q2.5-.5 4 1h1c3.8-.2 7.9 1 11 2h9c1.6 0 3 .8 4 1h32c1.2-.2 3.2-1 5-1h8a139 139 0 0 1 20-4l5-1c2-.7 3.7-1.5 6-2l4-1c1.5-.6 3-1.7 5-2h1q3-2.4 7-3h1q2.6-2.1 6-3h1c11.7-9.4 27.6-14.6 39-25 11.6-10.3 25-18.5 37-28a15 15 0 0 1-5 10Z"
/>
</svg>
);
<path
className="fill-primary stroke-primary"
d="M465 211h-1c-18.2 14.6-41.2 24.6-60 39-19 14.2-42.7 29.3-66 34l-4 1c-2.4 1-4 2-7 2h-1q-3.5 2-8 2h-1c-1.3 1.2-3 1.1-5 1h-2q-2.6 1.1-6 1h-2c-3 1.2-6.5 1-10 1-6.3.6-13.8.6-20 0-3.4 0-8.4.9-11-1h-1c-2.2.2-4.5.3-6-1h-1c-2 .2-3.7.2-5-1h-1c-7.6.5-16.5-3.4-23-6l-4-1a129 129 0 0 1-36.2-15.8c-10.4-6.6-23.2-12.8-32.5-20.5-9.2-7.7-23.8-12.8-30.3-22.7h-1c-2.3-1.4-4.5-2.7-6-5h-1c-4-2.5-8.5-5.2-12-8h-9a9 9 0 0 0-6 7c.3 3.3 0 6.7 0 10v9c.2 1.6 1 3.8 1 6v3c.2 1 1.2 2.2 1 4v1c1.2 1.2.8 2.2 1 4 .8 6.7 3 12.6 5 19 1.7 4.3 4.2 9.1 5 14v1q1.8 1.5 2 4v1a36 36 0 0 1 5 10c.7 2 1 3 2 5 8 12.7 15.7 25.5 25.8 37.3 10 11.7 20.8 20.6 32.4 30.4 11.7 9.9 28.3 14 39.8 23.3h1q2.5.3 4 2h1c2.8.4 4.8 2 7 3l7 2c5.7 1.3 13 2.3 18 5h1c2.1-.3 3.6.8 5 1h3c2.8.2 5.8 1 8 2h8c2.1 0 4.6.8 6 1h21c1.2-.2 3.2-1 5-1h9c3.3-1 7-2.4 11-2h1c2.7-2.2 7.4-2.4 11-3a55 55 0 0 0 8-2c6.5-2.6 13.9-6.3 21-8h1c8.5-6.8 20.6-9.7 29.2-16.8 8.7-7 18.3-12.8 26.8-20.2 4.4-3.8 9-9 13-13 14.8-14.8 20.7-34.6 33-50v-1q.9-3.4 3-6v-1q.3-2.5 2-4v-1c.5-3.3 2-8.6 4-11v-1q0-3.5 2-6v-1c1.1-6.7 2.4-15 5-21v-1c-.2-2-.2-3.7 1-5v-8c0-5.3-.5-10.8 0-16a14 14 0 0 0-4-6c-1-.5-1.1-.4-2-1h-6q-2.1 1.5-5 2m-6 38c-2.1 13.4-21.2 20.3-31 30-10 9.5-23.7 19-35 27-11.5 8-25.1 19.7-39 23h-1a22 22 0 0 1-10 4h-1a25 25 0 0 1-12 4h-1q-3.5 2-8 2h-1c-1.1 1.1-2.3 1-4 1h-2c-1.2.4-2.2 1-4 1h-2c-1.8.7-3.6 1.3-6 1h-1c-1.2 1.2-2.3 1-4 1h-5c-5.7.6-12.3.8-18 0h-4c-1.9 0-2.7-.6-4-1h-6c-1.9 0-2.7.3-4-1h-1q-2.5.5-4-1h-1c-8.1.5-16.8-3.6-24.2-5.8S210 329.8 204 325h-1c-12.8-5-27.1-15.6-37.7-24.3S138.8 284.2 131 273c-.3-.2-1 0-1 0-5.7-4.4-16.6-10-19-17-.9-2.6-1-5.4-2-8-.8-2.2-2.5-5-2-8a667 667 0 0 0 88 56h1q3.4.9 6 3h1c2.8.4 4.8 2 7 3q5 1.8 10 3l6 2q2.9.6 6 1 3 .4 5 1c1.6-.2 2 0 3 1h1c2-.2 3.7-.2 5 1h1c2.2-.3 3.4.4 5 1h8c1.6 0 3 .9 4 1h40c1.8-1.3 4.6-1.2 7-1h1c1.2-1.2 3.2-1.2 5-1h1c1.2-1.2 3.2-1.2 5-1h1c1.1-1.1 2.3-1 4-1h2c3.5-1.7 6.9-2.3 11-3l4-1c3.4-1.4 7.1-3 11-4 1.5-.4 2.5-.5 4-1 1.4-.7 2-1.9 4-2h1q2.6-2.1 6-3h1c2.5-2 6-3.8 9-5l3-1c1.4-.9 2-2.5 4-3h1q1.4-2.2 4-3h1c7.3-7.7 19-13.2 27.7-19.3 8.8-6.1 18.2-15 28.3-18.7.4-.2 1 0 1 0q3.8-3.9 9-6c1.3 2.5-.5 6.7-1 10m-20 55c-.2.4 0 1 0 1-3.4 9.6-12.7 19-19 27a88 88 0 0 1-12 12 214 214 0 0 1-26.7 20.3c-9.5 5.8-20 14.8-31.3 16.7h-1a22 22 0 0 1-10 4h-1c-3.2 2.6-8.9 3.3-13 4h-1q-1.5 1.4-4 1h-1q-1.5 1.4-4 1h-1c-4.9 2.3-10.5 1-16 2-1 .2-2.5 1-4 1-6.2.4-12.8.3-19 0-1.8 0-3.8-.8-5-1h-4c-1.6 0-3-.9-4-1h-4c-3.9-.3-8.8-1.3-12-3h-1c-3.3-.5-7.5-1-10-3h-1c-3.6-.1-8.4-1.8-11-4h-1c-3.9-.6-8-2.6-11-5h-1c-16.1-3.8-32.2-18.9-45-29a200 200 0 0 1-40-51c17.7 11.5 35 25.5 52 38h1c4 1.6 12.8 5.4 15 9h1c4.6 1 10.4 4.1 14 7h1q2.5.3 4 2h1c3.3.5 8.6 2 11 4h1q3.5 0 6 2h1q2.5-.5 4 1h1q2.5-.5 4 1h1c3.8-.2 7.9 1 11 2h9c1.6 0 3 .8 4 1h32c1.2-.2 3.2-1 5-1h8a139 139 0 0 1 20-4l5-1c2-.7 3.7-1.5 6-2l4-1c1.5-.6 3-1.7 5-2h1q3-2.4 7-3h1q2.6-2.1 6-3h1c11.7-9.4 27.6-14.6 39-25 11.6-10.3 25-18.5 37-28a15 15 0 0 1-5 10Z"
/>
</svg>
);
};
export const baseOptions: BaseLayoutProps = {
nav: {
// title: "Dokploy",
children: (
<Link href="/docs/core" className="flex items-center gap-2">
<Logo />
<span className="text-foreground font-semibold">Dokploy</span>
</Link>
),
},
links: [
{
text: "Login",
url: "https://app.dokploy.com/",
active: "nested-url",
icon: <LogIn />,
},
{
text: "Sign Up",
url: "https://app.dokploy.com/register",
active: "nested-url",
icon: <UserPlus />,
},
{
text: "Website",
url: "https://dokploy.com",
active: "nested-url",
icon: <GlobeIcon />,
},
{
text: "Discord",
url: "https://discord.com/invite/2tBnJ3jDJc",
active: "nested-url",
icon: (
<>
<svg
role="img"
className="size-6 "
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
</svg>
</>
),
},
{
text: "Support",
url: "https://opencollective.com/dokploy",
active: "nested-url",
icon: (
<>
<HeartIcon fill="currentColor" />
</>
),
},
{
text: "Github",
url: "https://github.com/dokploy/dokploy",
active: "nested-url",
icon: (
<>
<Github fill="currentColor" />
</>
),
},
{
text: "Blog",
url: "https://dokploy.com/blog",
active: "nested-url",
icon: (
<>
<Rss />
</>
),
},
],
nav: {
// title: "Dokploy",
children: (
<Link href="/docs/core" className="flex items-center gap-2">
<Logo />
<span className="text-foreground font-semibold">Dokploy</span>
</Link>
),
},
links: [
{
text: "Login",
url: "https://app.dokploy.com/",
active: "nested-url",
icon: <LogIn />,
},
{
text: "Sign Up",
url: "https://app.dokploy.com/register",
active: "nested-url",
icon: <UserPlus />,
},
{
text: "Website",
url: "https://dokploy.com",
active: "nested-url",
icon: <GlobeIcon />,
},
{
text: "Discord",
url: "https://discord.com/invite/2tBnJ3jDJc",
active: "nested-url",
icon: (
<>
<svg
role="img"
className="size-6 "
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
</svg>
</>
),
},
{
text: "Support",
url: "https://opencollective.com/dokploy",
active: "nested-url",
icon: (
<>
<HeartIcon fill="currentColor" />
</>
),
},
{
text: "Github",
url: "https://github.com/dokploy/dokploy",
active: "nested-url",
icon: (
<>
<Github fill="currentColor" />
</>
),
},
{
text: "Blog",
url: "https://dokploy.com/blog",
active: "nested-url",
icon: (
<>
<Rss />
</>
),
},
],
};

View File

@@ -4,19 +4,21 @@ import { Inter } from "next/font/google";
import type { ReactNode } from "react";
import { GoogleAnalytics } from "@next/third-parties/google";
const inter = Inter({
subsets: ["latin"],
subsets: ["latin"],
});
export default async function Layout({
children,
...rest
}: { children: ReactNode }) {
return (
<html lang="en" className={inter.className} suppressHydrationWarning>
<body className="flex flex-col min-h-screen">
<GoogleAnalytics gaId="G-HZ71HG38HN" />
<RootProvider>{children}</RootProvider>
</body>
</html>
);
children,
...rest
}: {
children: ReactNode;
}) {
return (
<html lang="en" className={inter.className} suppressHydrationWarning>
<body className="flex flex-col min-h-screen">
<GoogleAnalytics gaId="G-HZ71HG38HN" />
<RootProvider>{children}</RootProvider>
</body>
</html>
);
}

View File

@@ -1,11 +1,11 @@
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: "https://docs.dokploy.com/sitemap.xml",
};
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: "https://docs.dokploy.com/sitemap.xml",
};
}

View File

@@ -3,17 +3,17 @@ import { url } from "@/utils/metadata";
import type { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
return [
...(await Promise.all(
source.getPages().map(async (page) => {
const { lastModified } = page.data;
return {
url: url(page.url),
lastModified: lastModified ? new Date(lastModified) : undefined,
changeFrequency: "weekly",
priority: 0.5,
} as MetadataRoute.Sitemap[number];
}),
)),
];
return [
...(await Promise.all(
source.getPages().map(async (page) => {
const { lastModified } = page.data;
return {
url: url(page.url),
lastModified: lastModified ? new Date(lastModified) : undefined,
changeFrequency: "weekly",
priority: 0.5,
} as MetadataRoute.Sitemap[number];
}),
)),
];
}

View File

@@ -5,13 +5,13 @@ import { createOpenAPI } from "fumadocs-openapi/server";
import { attachFile } from "fumadocs-openapi/server";
export const source = loader({
baseUrl: "/docs",
source: createMDXSource(docs, meta),
// pageTree: {
// attachFile,
// },
baseUrl: "/docs",
source: createMDXSource(docs, meta),
// pageTree: {
// attachFile,
// },
});
export const openapi = createOpenAPI({
// options
// options
});

View File

@@ -1,7 +1,7 @@
import { defineConfig, defineDocs } from "fumadocs-mdx/config";
export const { docs, meta } = defineDocs({
dir: "content/docs",
dir: "content/docs",
});
export default defineConfig();

View File

@@ -1,30 +1,30 @@
import type { Metadata } from "next";
export const baseUrl =
process.env.NODE_ENV === "development"
? "http://localhost:3000"
: "https://docs.dokploy.com";
process.env.NODE_ENV === "development"
? "http://localhost:3000"
: "https://docs.dokploy.com";
export const url = (path: string): string => new URL(path, baseUrl).toString();
export function createMetadata(override: Metadata): Metadata {
return {
...override,
openGraph: {
title: override.title ?? undefined,
description: override.description ?? undefined,
url: "https://fumadocs.vercel.app",
images: "/og.png",
siteName: "Fumadocs",
...override.openGraph,
},
twitter: {
card: "summary_large_image",
creator: "@money_is_shark",
title: override.title ?? undefined,
description: override.description ?? undefined,
images: "/banner.png",
...override.twitter,
},
};
return {
...override,
openGraph: {
title: override.title ?? undefined,
description: override.description ?? undefined,
url: "https://fumadocs.vercel.app",
images: "/og.png",
siteName: "Fumadocs",
...override.openGraph,
},
twitter: {
card: "summary_large_image",
creator: "@money_is_shark",
title: override.title ?? undefined,
description: override.description ?? undefined,
images: "/banner.png",
...override.twitter,
},
};
}

View File

@@ -19,17 +19,20 @@ Open http://localhost:3000 with your browser to see the result.
## Environment Variables
### Required for Contact Form
```
RESEND_API_KEY=your_resend_api_key_here
```
### Required for HubSpot Integration (Sales Forms)
```
HUBSPOT_PORTAL_ID=147033433
HUBSPOT_FORM_GUID=0d788925-ef54-4fda-9b76-741fb5877056
```
### Required for Blog Page
```
GHOST_URL=""
GHOST_KEY=""

View File

@@ -1,31 +1,31 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { Resend } from "resend";
import { submitToHubSpot, getHubSpotUTK } from "@/lib/hubspot";
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { Resend } from 'resend'
import { submitToHubSpot, getHubSpotUTK } from '@/lib/hubspot'
interface ContactFormData {
inquiryType: "support" | "sales" | "other";
firstName: string;
lastName: string;
email: string;
company: string;
message: string;
inquiryType: 'support' | 'sales' | 'other'
firstName: string
lastName: string
email: string
company: string
message: string
}
export async function POST(request: NextRequest) {
try {
// Initialize Resend with API key check
const apiKey = process.env.RESEND_API_KEY;
const apiKey = process.env.RESEND_API_KEY
if (!apiKey) {
console.error("RESEND_API_KEY is not configured");
console.error('RESEND_API_KEY is not configured')
return NextResponse.json(
{ error: "Email service not configured" },
{ error: 'Email service not configured' },
{ status: 500 },
);
)
}
const resend = new Resend(apiKey);
const body: ContactFormData = await request.json();
const resend = new Resend(apiKey)
const body: ContactFormData = await request.json()
// Validate required fields
if (
@@ -37,41 +37,45 @@ export async function POST(request: NextRequest) {
!body.message
) {
return NextResponse.json(
{ error: "All fields are required" },
{ error: 'All fields are required' },
{ status: 400 },
);
)
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(body.email)) {
return NextResponse.json(
{ error: "Invalid email format" },
{ error: 'Invalid email format' },
{ status: 400 },
);
)
}
// Submit to HubSpot if it's a sales inquiry
if (body.inquiryType === "sales") {
if (body.inquiryType === 'sales') {
try {
const hutk = getHubSpotUTK(request.headers.get("cookie") || undefined);
const hubspotSuccess = await submitToHubSpot(body, hutk);
const hutk = getHubSpotUTK(
request.headers.get('cookie') || undefined,
)
const hubspotSuccess = await submitToHubSpot(body, hutk)
if (hubspotSuccess) {
console.log("Successfully submitted sales inquiry to HubSpot");
console.log(
'Successfully submitted sales inquiry to HubSpot',
)
} else {
console.warn(
"Failed to submit sales inquiry to HubSpot, but continuing with email",
);
'Failed to submit sales inquiry to HubSpot, but continuing with email',
)
}
} catch (error) {
console.error("Error submitting to HubSpot:", error);
console.error('Error submitting to HubSpot:', error)
// Continue with email even if HubSpot fails
}
}
// Format email content
const emailSubject = `[${body.inquiryType.toUpperCase()}] New contact form submission from ${body.firstName} ${body.lastName}`;
const emailSubject = `[${body.inquiryType.toUpperCase()}] New contact form submission from ${body.firstName} ${body.lastName}`
const emailBody = `
New contact form submission:
@@ -86,23 +90,23 @@ ${body.message}
---
Sent from Dokploy website contact form
`.trim();
`.trim()
// Send email to Dokploy team
await resend.emails.send({
from: "Dokploy Contact Form <noreply@emails.dokploy.com>",
from: 'Dokploy Contact Form <noreply@emails.dokploy.com>',
to:
body.inquiryType === "sales"
? ["sales@dokploy.com", "contact@dokploy.com"]
: ["contact@dokploy.com"],
body.inquiryType === 'sales'
? ['sales@dokploy.com', 'contact@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";
'Thank you for contacting Dokploy - We received your message'
const confirmationBody = `
Hello ${body.firstName} ${body.lastName},
@@ -122,24 +126,24 @@ 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();
`.trim()
await resend.emails.send({
from: "Dokploy Team <noreply@emails.dokploy.com>",
from: 'Dokploy Team <noreply@emails.dokploy.com>',
to: [body.email],
subject: confirmationSubject,
text: confirmationBody,
});
})
return NextResponse.json(
{ message: "Contact form submitted successfully" },
{ message: 'Contact form submitted successfully' },
{ status: 200 },
);
)
} catch (error) {
console.error("Error processing contact form:", error);
console.error('Error processing contact form:', error)
return NextResponse.json(
{ error: "Internal server error" },
{ error: 'Internal server error' },
{ status: 500 },
);
)
}
}

View File

@@ -1,34 +1,32 @@
import { NextResponse } from "next/server";
import { NextResponse } from 'next/server'
// Cache the result for 5 minutes to avoid rate limiting
let cachedStars: { count: number; timestamp: number } | null = null;
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
let cachedStars: { count: number; timestamp: number } | null = null
const CACHE_DURATION = 5 * 60 * 1000 // 5 minutes in milliseconds
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const owner = searchParams.get("owner");
const repo = searchParams.get("repo");
const { searchParams } = new URL(request.url)
const owner = searchParams.get('owner')
const repo = searchParams.get('repo')
if (!owner || !repo) {
return NextResponse.json(
{ error: "Owner and repo parameters are required" },
{ error: 'Owner and repo parameters are required' },
{ status: 400 },
);
)
}
// Check if we have a valid cached result
if (
cachedStars &&
Date.now() - cachedStars.timestamp < CACHE_DURATION
) {
if (cachedStars && Date.now() - cachedStars.timestamp < CACHE_DURATION) {
return NextResponse.json(
{ stargazers_count: cachedStars.count },
{
headers: {
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
'Cache-Control':
'public, s-maxage=300, stale-while-revalidate=600',
},
},
);
)
}
try {
@@ -36,42 +34,42 @@ export async function GET(request: Request) {
`https://api.github.com/repos/${owner}/${repo}`,
{
headers: {
Accept: "application/vnd.github.v3+json",
"User-Agent": "Dokploy-Website",
Accept: 'application/vnd.github.v3+json',
'User-Agent': 'Dokploy-Website',
},
},
);
)
if (!response.ok) {
return NextResponse.json(
{ error: "Failed to fetch repository data" },
{ error: 'Failed to fetch repository data' },
{ status: response.status },
);
)
}
const data = await response.json();
const starCount = data.stargazers_count;
const data = await response.json()
const starCount = data.stargazers_count
// Cache the result
cachedStars = {
count: starCount,
timestamp: Date.now(),
};
}
return NextResponse.json(
{ stargazers_count: starCount },
{
headers: {
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
'Cache-Control':
'public, s-maxage=300, stale-while-revalidate=600',
},
},
);
)
} catch (error) {
console.error("Error fetching GitHub stars:", error);
console.error('Error fetching GitHub stars:', error)
return NextResponse.json(
{ error: "Internal server error" },
{ error: 'Internal server error' },
{ status: 500 },
);
)
}
}

View File

@@ -1,35 +1,34 @@
import { getPost } from "@/lib/ghost";
import { generateOGImage } from "@/lib/og-image";
import type { NextRequest } from "next/server";
import { getPost } from '@/lib/ghost'
import { generateOGImage } from '@/lib/og-image'
import type { NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const slug = searchParams.get("slug");
const { searchParams } = new URL(request.url)
const slug = searchParams.get('slug')
console.log("Generating OG image for slug:", slug);
console.log('Generating OG image for slug:', slug)
if (!slug) {
console.error("Missing slug parameter");
return new Response("Missing slug parameter", { status: 400 });
console.error('Missing slug parameter')
return new Response('Missing slug parameter', { status: 400 })
}
const post = await getPost(slug);
const post = await getPost(slug)
if (!post) {
console.error("Post not found for slug:", slug);
return new Response("Post not found", { status: 404 });
console.error('Post not found for slug:', slug)
return new Response('Post not found', { status: 404 })
}
const formattedDate = new Date(post.published_at).toLocaleDateString(
"en-US",
'en-US',
{
year: "numeric",
month: "long",
day: "numeric",
year: 'numeric',
month: 'long',
day: 'numeric',
},
);
)
const ogImage = await generateOGImage({
title: post.title,
@@ -41,16 +40,16 @@ export async function GET(request: NextRequest) {
: undefined,
date: formattedDate,
readingTime: post.reading_time,
});
})
return new Response(ogImage, {
headers: {
"Content-Type": "image/png",
"Cache-Control": "public, max-age=31536000, immutable",
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=31536000, immutable',
},
});
})
} catch (error) {
console.error("Error generating OG image:", error);
return new Response(`Error generating image: ${error}`, { status: 500 });
console.error('Error generating OG image:', error)
return new Response(`Error generating image: ${error}`, { status: 500 })
}
}

View File

@@ -1,39 +1,39 @@
"use client";
'use client'
import { CopyButton } from "@/components/ui/copy-button";
import * as babel from "prettier/plugins/babel";
import * as estree from "prettier/plugins/estree";
import * as yaml from "prettier/plugins/yaml";
import * as prettier from "prettier/standalone";
import { type JSX, useLayoutEffect, useState } from "react";
import type { BundledLanguage } from "shiki/bundle/web";
import { highlight } from "./shared";
import { CopyButton } from '@/components/ui/copy-button'
import * as babel from 'prettier/plugins/babel'
import * as estree from 'prettier/plugins/estree'
import * as yaml from 'prettier/plugins/yaml'
import * as prettier from 'prettier/standalone'
import { type JSX, useLayoutEffect, useState } from 'react'
import type { BundledLanguage } from 'shiki/bundle/web'
import { highlight } from './shared'
interface CodeBlockProps {
code: string;
lang: BundledLanguage;
initial?: JSX.Element;
code: string
lang: BundledLanguage
initial?: JSX.Element
}
async function formatCode(code: string, lang: string) {
try {
let parser: string;
let plugins = [] as any[];
let parser: string
let plugins = [] as any[]
switch (lang.toLowerCase()) {
case "yaml":
case "yml":
parser = "yaml";
plugins = [yaml];
break;
case "javascript":
case "typescript":
case "jsx":
case "tsx":
parser = "babel-ts";
plugins = [babel, estree];
break;
case 'yaml':
case 'yml':
parser = 'yaml'
plugins = [yaml]
break
case 'javascript':
case 'typescript':
case 'jsx':
case 'tsx':
parser = 'babel-ts'
plugins = [babel, estree]
break
default:
return code;
return code
}
const formatted = await prettier.format(code, {
parser,
@@ -43,50 +43,50 @@ async function formatCode(code: string, lang: string) {
tabWidth: 2,
useTabs: false,
printWidth: 120,
});
return formatted;
})
return formatted
} catch (error) {
console.error("Error formatting code:", error);
return code;
console.error('Error formatting code:', error)
return code
}
}
export function CodeBlock({ code, lang, initial }: CodeBlockProps) {
const [nodes, setNodes] = useState<JSX.Element | undefined>(initial);
const [formattedCode, setFormattedCode] = useState(code);
const [nodes, setNodes] = useState<JSX.Element | undefined>(initial)
const [formattedCode, setFormattedCode] = useState(code)
useLayoutEffect(() => {
async function formatAndHighlight() {
try {
const formatted = await formatCode(code, lang);
setFormattedCode(formatted);
const highlighted = await highlight(formatted, lang);
setNodes(highlighted);
const formatted = await formatCode(code, lang)
setFormattedCode(formatted)
const highlighted = await highlight(formatted, lang)
setNodes(highlighted)
} catch (error) {
const highlighted = await highlight(code, lang);
setNodes(highlighted);
const highlighted = await highlight(code, lang)
setNodes(highlighted)
}
}
void formatAndHighlight();
}, [code, lang]);
void formatAndHighlight()
}, [code, lang])
if (!nodes) {
return (
<div className="group relative">
<div className="text-sm p-4 rounded-lg bg-[#18191F] overflow-auto animate-pulse">
<div className="h-4 bg-gray-700 rounded w-3/4 mb-2" />
<div className="h-4 bg-gray-700 rounded w-1/2" />
<div className="animate-pulse overflow-auto rounded-lg bg-[#18191F] p-4 text-sm">
<div className="mb-2 h-4 w-3/4 rounded bg-gray-700" />
<div className="h-4 w-1/2 rounded bg-gray-700" />
</div>
</div>
);
)
}
return (
<div className="group relative">
<CopyButton text={formattedCode} />
<div className="text-sm p-4 rounded-lg bg-[#18191F] overflow-auto">
<div className="overflow-auto rounded-lg bg-[#18191F] p-4 text-sm">
{nodes}
</div>
</div>
);
)
}

View File

@@ -1,18 +1,18 @@
"use client";
'use client'
import { useRouter } from "next/navigation";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import slugify from "slugify";
import { useRouter } from 'next/navigation'
import type { DetailedHTMLProps, HTMLAttributes } from 'react'
import slugify from 'slugify'
type HeadingProps = DetailedHTMLProps<
HTMLAttributes<HTMLHeadingElement>,
HTMLHeadingElement
>;
>
function LinkIcon() {
return (
<svg
className="inline-block w-4 h-4 ml-2 opacity-0 group-hover:opacity-100 transition-opacity"
className="ml-2 inline-block h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -24,62 +24,71 @@ function LinkIcon() {
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
/>
</svg>
);
)
}
export function H1({ children, ...props }: HeadingProps) {
const router = useRouter();
const id = slugify(children?.toString() || "", { lower: true, strict: true });
const router = useRouter()
const id = slugify(children?.toString() || '', {
lower: true,
strict: true,
})
const handleClick = () => {
router.push(`#${id}`);
};
router.push(`#${id}`)
}
return (
<h1
id={id}
onClick={handleClick}
className="group text-xl md:text-2xl xl:text-3xl text-primary font-bold mt-8 mb-4 cursor-pointer hover:text-primary/80 transition-colors"
className="group mb-4 mt-8 cursor-pointer text-xl font-bold text-primary transition-colors hover:text-primary/80 md:text-2xl xl:text-3xl"
{...props}
>
{children}
<LinkIcon />
</h1>
);
)
}
export function H2({ children, ...props }: HeadingProps) {
const router = useRouter();
const id = slugify(children?.toString() || "", { lower: true, strict: true });
const router = useRouter()
const id = slugify(children?.toString() || '', {
lower: true,
strict: true,
})
const handleClick = () => {
router.push(`#${id}`);
};
router.push(`#${id}`)
}
return (
<h2
id={id}
onClick={handleClick}
className="group text-2xl text-primary/90 font-semibold mt-6 mb-3 cursor-pointer hover:text-primary/80 transition-colors"
className="group mb-3 mt-6 cursor-pointer text-2xl font-semibold text-primary/90 transition-colors hover:text-primary/80"
{...props}
>
{children}
<LinkIcon />
</h2>
);
)
}
export function H3({ children, ...props }: HeadingProps) {
const router = useRouter();
const id = slugify(children?.toString() || "", { lower: true, strict: true });
const router = useRouter()
const id = slugify(children?.toString() || '', {
lower: true,
strict: true,
})
const handleClick = () => {
router.push(`#${id}`);
};
router.push(`#${id}`)
}
return (
<h3
id={id}
onClick={handleClick}
className="group text-xl text-primary/90 font-semibold mt-4 mb-2 cursor-pointer hover:text-primary/80 transition-colors"
className="group mb-2 mt-4 cursor-pointer text-xl font-semibold text-primary/90 transition-colors hover:text-primary/80"
{...props}
>
{children}
<LinkIcon />
</h3>
);
)
}

View File

@@ -1,65 +1,67 @@
"use client";
'use client'
import { useEffect, useState } from "react";
import { useEffect, useState } from 'react'
interface Heading {
id: string;
text: string;
level: number;
id: string
text: string
level: number
}
export function TableOfContents() {
const [headings, setHeadings] = useState<Heading[]>([]);
const [activeId, setActiveId] = useState<string>();
const [headings, setHeadings] = useState<Heading[]>([])
const [activeId, setActiveId] = useState<string>()
useEffect(() => {
const elements = Array.from(document.querySelectorAll("h1, h2, h3"))
const elements = Array.from(document.querySelectorAll('h1, h2, h3'))
.filter((element) => element.id)
.map((element) => ({
id: element.id,
text: element.textContent || "",
text: element.textContent || '',
level: Number(element.tagName.charAt(1)),
}));
setHeadings(elements);
}))
setHeadings(elements)
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
setActiveId(entry.target.id);
setActiveId(entry.target.id)
}
}
},
{ rootMargin: "-100px 0px -66%" },
);
{ rootMargin: '-100px 0px -66%' },
)
for (const { id } of elements) {
const element = document.getElementById(id);
if (element) observer.observe(element);
const element = document.getElementById(id)
if (element) observer.observe(element)
}
return () => observer.disconnect();
}, []);
return () => observer.disconnect()
}, [])
return (
<nav className="space-y-2 text-sm">
<p className="font-medium mb-4">Table of Contents</p>
<p className="mb-4 font-medium">Table of Contents</p>
<ul className="space-y-2">
{headings.length > 0 ? (
headings.map((heading) => (
<li
key={heading.id}
style={{ paddingLeft: `${(heading.level - 1) * 1}rem` }}
style={{
paddingLeft: `${(heading.level - 1) * 1}rem`,
}}
>
<a
href={`#${heading.id}`}
onClick={(e) => {
e.preventDefault();
e.preventDefault()
document
.getElementById(heading.id)
?.scrollIntoView({ behavior: "smooth" });
?.scrollIntoView({ behavior: 'smooth' })
}}
className={`hover:text-primary transition-colors block ${activeId === heading.id ? "text-primary font-medium" : "text-muted-foreground"}`}
className={`block transition-colors hover:text-primary ${activeId === heading.id ? 'font-medium text-primary' : 'text-muted-foreground'}`}
>
{heading.text}
</a>
@@ -67,10 +69,12 @@ export function TableOfContents() {
))
) : (
<li>
<p className="text-muted-foreground">No headings found</p>
<p className="text-muted-foreground">
No headings found
</p>
</li>
)}
</ul>
</nav>
);
)
}

View File

@@ -1,21 +1,25 @@
"use client";
'use client'
import { cn } from "@/lib/utils";
import { PhotoProvider, PhotoView } from "react-photo-view";
import "react-photo-view/dist/react-photo-view.css";
import { cn } from '@/lib/utils'
import { PhotoProvider, PhotoView } from 'react-photo-view'
import 'react-photo-view/dist/react-photo-view.css'
interface ZoomableImageProps {
src: string;
alt: string;
className?: string;
src: string
alt: string
className?: string
}
export function ZoomableImage({ src, alt, className }: ZoomableImageProps) {
return (
<PhotoProvider>
<PhotoView src={src}>
<img src={src} alt={alt} className={cn("object-cover", className)} />
<img
src={src}
alt={alt}
className={cn('object-cover', className)}
/>
</PhotoView>
</PhotoProvider>
);
)
}

View File

@@ -1,14 +1,14 @@
import { toJsxRuntime } from "hast-util-to-jsx-runtime";
import type { JSX } from "react";
import { Fragment } from "react";
import { jsx, jsxs } from "react/jsx-runtime";
import type { BundledLanguage } from "shiki/bundle/web";
import { codeToHast } from "shiki/bundle/web";
import { toJsxRuntime } from 'hast-util-to-jsx-runtime'
import type { JSX } from 'react'
import { Fragment } from 'react'
import { jsx, jsxs } from 'react/jsx-runtime'
import type { BundledLanguage } from 'shiki/bundle/web'
import { codeToHast } from 'shiki/bundle/web'
export async function highlight(code: string, lang: BundledLanguage) {
const out = await codeToHast(code, {
lang,
theme: "houston",
});
return toJsxRuntime(out, { Fragment, jsx, jsxs }) as JSX.Element;
theme: 'houston',
})
return toJsxRuntime(out, { Fragment, jsx, jsxs }) as JSX.Element
}

View File

@@ -1,47 +1,47 @@
import { getPost, getPosts } from "@/lib/ghost";
import type { Metadata, ResolvingMetadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import type React from "react";
import ReactMarkdown from "react-markdown";
import type { Components } from "react-markdown";
import rehypeRaw from "rehype-raw";
import remarkGfm from "remark-gfm";
import remarkToc from "remark-toc";
import type { BundledLanguage } from "shiki/bundle/web";
import TurndownService from "turndown";
import { getPost, getPosts } from '@/lib/ghost'
import type { Metadata, ResolvingMetadata } from 'next'
import Image from 'next/image'
import Link from 'next/link'
import { notFound } from 'next/navigation'
import type React from 'react'
import ReactMarkdown from 'react-markdown'
import type { Components } from 'react-markdown'
import rehypeRaw from 'rehype-raw'
import remarkGfm from 'remark-gfm'
import remarkToc from 'remark-toc'
import type { BundledLanguage } from 'shiki/bundle/web'
import TurndownService from 'turndown'
// @ts-ignore
import * as turndownPluginGfm from "turndown-plugin-gfm";
import { CodeBlock } from "./components/CodeBlock";
import { H1, H2, H3 } from "./components/Headings";
import { TableOfContents } from "./components/TableOfContents";
import { ZoomableImage } from "./components/ZoomableImage";
import * as turndownPluginGfm from 'turndown-plugin-gfm'
import { CodeBlock } from './components/CodeBlock'
import { H1, H2, H3 } from './components/Headings'
import { TableOfContents } from './components/TableOfContents'
import { ZoomableImage } from './components/ZoomableImage'
type Props = {
params: { slug: string };
};
params: { slug: string }
}
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata,
): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
const { slug } = await params
const post = await getPost(slug)
if (!post) {
return {
title: "Post Not Found",
};
title: 'Post Not Found',
}
}
const ogUrl = new URL(
`/api/og`,
process.env.NODE_ENV === "production"
? "https://dokploy.com"
: "http://localhost:3000",
);
ogUrl.searchParams.set("slug", slug);
process.env.NODE_ENV === 'production'
? 'https://dokploy.com'
: 'http://localhost:3000',
)
ogUrl.searchParams.set('slug', slug)
return {
title: post.title,
@@ -49,7 +49,7 @@ export async function generateMetadata(
openGraph: {
title: post.title,
description: post.custom_excerpt || post.excerpt,
type: "article",
type: 'article',
url: `${process.env.NEXT_PUBLIC_APP_URL}/blog/${post.slug}`,
images: [
{
@@ -61,66 +61,66 @@ export async function generateMetadata(
],
},
twitter: {
card: "summary_large_image",
card: 'summary_large_image',
title: post.title,
description: post.custom_excerpt || post.excerpt,
images: [ogUrl.toString()],
},
};
}
}
export default async function BlogPostPage({ params }: Props) {
const { slug } = await params;
const post = await getPost(slug);
const allPosts = await getPosts();
const { slug } = await params
const post = await getPost(slug)
const allPosts = await getPosts()
const relatedPosts = allPosts.filter((p) => p.id !== post?.id).slice(0, 3);
const relatedPosts = allPosts.filter((p) => p.id !== post?.id).slice(0, 3)
if (!post) {
notFound();
notFound()
}
const cleanHtml = (html: string) => {
if (typeof window !== "undefined") {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
if (typeof window !== 'undefined') {
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
const scripts = doc.querySelectorAll(
'script[type="application/ld+json"], script',
);
scripts.forEach((script) => script.remove());
const unwantedElements = doc.querySelectorAll("style, meta, link");
unwantedElements.forEach((el) => el.remove());
return doc.body.innerHTML;
)
scripts.forEach((script) => script.remove())
const unwantedElements = doc.querySelectorAll('style, meta, link')
unwantedElements.forEach((el) => el.remove())
return doc.body.innerHTML
} else {
return html
.replace(
/<script[^>]*type="application\/ld\+json"[^>]*>[\s\S]*?<\/script>/gi,
"",
'',
)
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
.replace(/<meta[^>]*>/gi, "")
.replace(/<link[^>]*>/gi, "");
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
.replace(/<meta[^>]*>/gi, '')
.replace(/<link[^>]*>/gi, '')
}
};
}
const turndownService = new TurndownService({
headingStyle: "atx",
codeBlockStyle: "fenced",
});
const gfm = turndownPluginGfm.gfm;
const tables = turndownPluginGfm.tables;
const strikethrough = turndownPluginGfm.strikethrough;
turndownService.use([tables, strikethrough, gfm, remarkToc]);
headingStyle: 'atx',
codeBlockStyle: 'fenced',
})
const gfm = turndownPluginGfm.gfm
const tables = turndownPluginGfm.tables
const strikethrough = turndownPluginGfm.strikethrough
turndownService.use([tables, strikethrough, gfm, remarkToc])
const cleanedHtml = cleanHtml(post.html);
const markdown = turndownService.turndown(cleanedHtml);
const cleanedHtml = cleanHtml(post.html)
const markdown = turndownService.turndown(cleanedHtml)
const formattedDate = new Date(post.published_at).toLocaleDateString("en", {
year: "numeric",
month: "long",
day: "numeric",
});
const formattedDate = new Date(post.published_at).toLocaleDateString('en', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
const components: Partial<Components> = {
h1: H1,
@@ -128,7 +128,7 @@ export default async function BlogPostPage({ params }: Props) {
h3: H3,
p: ({ node, children, ...props }) => (
<p
className="text-base text-muted-foreground leading-relaxed mb-4"
className="mb-4 text-base leading-relaxed text-muted-foreground"
{...props}
>
{children}
@@ -137,7 +137,7 @@ export default async function BlogPostPage({ params }: Props) {
a: ({ node, href, ...props }) => (
<a
href={href}
className="text-blue-500 hover:text-blue-500/80 transition-colors"
className="text-blue-500 transition-colors hover:text-blue-500/80"
target="_blank"
rel="noopener noreferrer"
{...props}
@@ -145,32 +145,32 @@ export default async function BlogPostPage({ params }: Props) {
),
ul: ({ node, ...props }) => (
<ul
className="list-disc pl-6 space-y-1 mb-4 text-muted-foreground"
className="mb-4 list-disc space-y-1 pl-6 text-muted-foreground"
{...props}
/>
),
ol: ({ node, ...props }) => (
<ol
className="list-decimal pl-6 space-y-1 mb-4 text-muted-foreground"
className="mb-4 list-decimal space-y-1 pl-6 text-muted-foreground"
{...props}
/>
),
li: ({ node, ...props }) => (
<li className="text-base leading-relaxed ml-2" {...props} />
<li className="ml-2 text-base leading-relaxed" {...props} />
),
blockquote: ({ node, ...props }) => (
<blockquote
className="border-l-4 border-primary pl-4 py-2 my-4 bg-muted/50"
className="my-4 border-l-4 border-primary bg-muted/50 py-2 pl-4"
{...props}
/>
),
table: ({ node, ...props }) => (
<div className="my-6 w-full overflow-x-auto border rounded-lg">
<div className="my-6 w-full overflow-x-auto rounded-lg border">
<table className="w-full border-collapse" {...props} />
</div>
),
thead: ({ node, ...props }) => (
<thead className="bg-muted border-b border-border" {...props} />
<thead className="border-b border-border bg-muted" {...props} />
),
tbody: ({ node, ...props }) => (
<tbody className="divide-y divide-border" {...props} />
@@ -186,42 +186,46 @@ export default async function BlogPostPage({ params }: Props) {
),
img: ({ node, src, alt }) => (
<ZoomableImage
src={src || ""}
alt={alt || ""}
className="object-cover max-w-lg mx-auto rounded-lg border max-lg:w-64 border-border overflow-hidden"
src={src || ''}
alt={alt || ''}
className="mx-auto max-w-lg overflow-hidden rounded-lg border border-border object-cover max-lg:w-64"
/>
),
code: ({
className,
children,
inline,
}: { className: string; children: React.ReactNode; inline: boolean }) => {
}: {
className: string
children: React.ReactNode
inline: boolean
}) => {
if (inline || !className || !/language-(\w+)/.test(className)) {
return (
<code className="px-1.5 py-0.5 bg-muted text-sm rounded font-mono text-foreground">
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-sm text-foreground">
{children}
</code>
);
)
}
const match = /language-(\w+)/.exec(className);
const match = /language-(\w+)/.exec(className)
return (
<CodeBlock
lang={match ? (match[1] as BundledLanguage) : "ts"}
code={children?.toString() || ""}
lang={match ? (match[1] as BundledLanguage) : 'ts'}
code={children?.toString() || ''}
/>
);
)
},
};
}
return (
<article className="mx-auto px-4 sm:px-6 lg:px-8 pb-12 max-w-7xl w-full">
<article className="mx-auto w-full max-w-7xl px-4 pb-12 sm:px-6 lg:px-8">
<Link
href="/blog"
className="inline-flex items-center mb-8 text-primary hover:text-primary/80 transition-colors"
className="mb-8 inline-flex items-center text-primary transition-colors hover:text-primary/80"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 mr-2"
className="mr-2 h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
@@ -234,15 +238,15 @@ export default async function BlogPostPage({ params }: Props) {
Back to Blog
</Link>
<div className="grid grid-cols-1 lg:grid-cols-[1fr_250px] gap-8">
<div className="rounded-lg p-8 shadow-lg border border-border">
<div className="grid grid-cols-1 gap-8 lg:grid-cols-[1fr_250px]">
<div className="rounded-lg border border-border p-8 shadow-lg">
<header className="mb-8">
<h1 className="text-xl md:text-2xl xl:text-3xl font-bold mb-4">
<h1 className="mb-4 text-xl font-bold md:text-2xl xl:text-3xl">
{post.title}
</h1>
<div className="flex items-center mb-6">
<div className="mb-6 flex items-center">
{post.primary_author?.profile_image && (
<div className="relative h-12 w-12 rounded-full overflow-hidden mr-4">
<div className="relative mr-4 h-12 w-12 overflow-hidden rounded-full">
{post.primary_author.twitter ? (
<a
href={`https://twitter.com/${post.primary_author.twitter}`}
@@ -251,14 +255,20 @@ export default async function BlogPostPage({ params }: Props) {
className="block cursor-pointer transition-opacity hover:opacity-90"
>
<img
src={post.primary_author.profile_image}
src={
post.primary_author
.profile_image
}
alt={post.primary_author.name}
className="object-cover"
/>
</a>
) : (
<img
src={post.primary_author.profile_image}
src={
post.primary_author
.profile_image
}
alt={post.primary_author.name}
className="object-cover"
/>
@@ -272,25 +282,28 @@ export default async function BlogPostPage({ params }: Props) {
href={`https://twitter.com/${post.primary_author.twitter}`}
target="_blank"
rel="noopener noreferrer"
className="hover:text-primary transition-colors"
className="transition-colors hover:text-primary"
>
{post.primary_author.name || "Unknown Author"}
{post.primary_author.name ||
'Unknown Author'}
</a>
) : (
post.primary_author?.name || "Unknown Author"
post.primary_author?.name ||
'Unknown Author'
)}
</p>
<p className="text-sm text-muted-foreground">
{formattedDate} {post.reading_time} min read
{formattedDate} {post.reading_time} min
read
</p>
</div>
</div>
{post.feature_image && (
<div className="relative w-full h-[400px] mb-8">
<div className="relative mb-8 h-[400px] w-full">
<ZoomableImage
src={post.feature_image}
alt={post.title}
className="rounded-lg h-full w-full object-cover"
className="h-full w-full rounded-lg object-cover"
/>
</div>
)}
@@ -310,14 +323,14 @@ export default async function BlogPostPage({ params }: Props) {
</div>
{post.tags && post.tags.length > 0 && (
<div className="mt-12 pt-6 border-t border-border">
<h2 className="text-xl font-semibold mb-4">Tags</h2>
<div className="mt-12 border-t border-border pt-6">
<h2 className="mb-4 text-xl font-semibold">Tags</h2>
<div className="flex flex-wrap gap-2">
{post.tags.map((tag) => (
<Link
key={tag.id}
href={`/blog/tag/${tag.slug}`}
className="px-4 py-2 bg-muted hover:bg-muted/80 rounded-full text-sm transition-colors"
className="rounded-full bg-muted px-4 py-2 text-sm transition-colors hover:bg-muted/80"
>
{tag.name}
</Link>
@@ -327,7 +340,7 @@ export default async function BlogPostPage({ params }: Props) {
)}
</div>
<div className="hidden lg:block max-w-[16rem]">
<div className="hidden max-w-[16rem] lg:block">
<div className="sticky top-4">
<TableOfContents />
</div>
@@ -336,16 +349,16 @@ export default async function BlogPostPage({ params }: Props) {
{relatedPosts.length > 0 && (
<div className="mt-12">
<h2 className="text-2xl font-bold mb-6">Related Posts</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<h2 className="mb-6 text-2xl font-bold">Related Posts</h2>
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
{relatedPosts.map((relatedPost) => {
const relatedPostDate = new Date(
relatedPost.published_at,
).toLocaleDateString("en", {
year: "numeric",
month: "long",
day: "numeric",
});
).toLocaleDateString('en', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
return (
<Link
@@ -353,34 +366,39 @@ export default async function BlogPostPage({ params }: Props) {
href={`/blog/${relatedPost.slug}`}
className="group"
>
<div className="bg-card rounded-lg overflow-hidden h-full shadow-lg transition-all duration-300 hover:shadow-xl border border-border">
<div className="h-full overflow-hidden rounded-lg border border-border bg-card shadow-lg transition-all duration-300 hover:shadow-xl">
{relatedPost.feature_image && (
<div className="relative w-full">
<img
src={relatedPost.feature_image || "/og.png"}
src={
relatedPost.feature_image ||
'/og.png'
}
alt={relatedPost.title}
className="object-cover "
/>
</div>
)}
<div className="p-6">
<h3 className="text-lg font-semibold mb-2 group-hover:text-primary transition-colors line-clamp-2">
<h3 className="mb-2 line-clamp-2 text-lg font-semibold transition-colors group-hover:text-primary">
{relatedPost.title}
</h3>
<p className="text-sm text-muted-foreground mb-4">
{relatedPostDate} {relatedPost.reading_time} min read
<p className="mb-4 text-sm text-muted-foreground">
{relatedPostDate} {' '}
{relatedPost.reading_time} min
read
</p>
<p className="text-muted-foreground line-clamp-2">
<p className="line-clamp-2 text-muted-foreground">
{relatedPost.excerpt}
</p>
</div>
</div>
</Link>
);
)
})}
</div>
</div>
)}
</article>
);
)
}

View File

@@ -1,53 +1,53 @@
"use client";
'use client'
import type { Post } from "@/lib/ghost";
import Link from "next/link";
import { useRouter } from "next/navigation";
import type { Post } from '@/lib/ghost'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
interface BlogPostCardProps {
post: Post;
post: Post
}
export function BlogPostCard({ post }: BlogPostCardProps) {
const router = useRouter();
const formattedDate = new Date(post.published_at).toLocaleDateString("en", {
year: "numeric",
month: "long",
day: "numeric",
});
const router = useRouter()
const formattedDate = new Date(post.published_at).toLocaleDateString('en', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
const handleTwitterClick = (e: React.MouseEvent) => {
if (post.primary_author?.twitter) {
router.push(`https://twitter.com/${post.primary_author.twitter}`);
router.push(`https://twitter.com/${post.primary_author.twitter}`)
}
e.preventDefault();
e.stopPropagation();
};
e.preventDefault()
e.stopPropagation()
}
return (
<Link
href={`/blog/${post.slug}`}
className="group block hover:bg-muted p-4 rounded-lg border border-border"
className="group block rounded-lg border border-border p-4 hover:bg-muted"
>
<article className="flex gap-6 items-start max-sm:flex-col items-center">
<div className="relative shrink-0 flex items-center justify-center mx-auto">
<article className="flex items-start items-center gap-6 max-sm:flex-col">
<div className="relative mx-auto flex shrink-0 items-center justify-center">
<img
src={post.feature_image || "/og.png"}
alt={post.feature_image ? post.title : "Default Image"}
className="object-cover rounded-lg object-center mx-auto self-center h-32 w-64 sm:w-32 sm:h-24"
src={post.feature_image || '/og.png'}
alt={post.feature_image ? post.title : 'Default Image'}
className="mx-auto h-32 w-64 self-center rounded-lg object-cover object-center sm:h-24 sm:w-32"
/>
</div>
<div className="w-full flex-wrap flex">
<h2 className="text-xl font-semibold mb-2 group-hover:text-primary">
<div className="flex w-full flex-wrap">
<h2 className="mb-2 text-xl font-semibold group-hover:text-primary">
{post.title}
</h2>
<p className="text-muted-foreground line-clamp-2 mb-4">
<p className="mb-4 line-clamp-2 text-muted-foreground">
{post.custom_excerpt || post.excerpt}
</p>
<div className="flex items-center text-sm text-muted-foreground flex-wrap">
<div className="flex flex-wrap items-center text-sm text-muted-foreground">
<div className="flex items-center">
{post.primary_author?.profile_image && (
<div className="relative h-6 w-6 rounded-full overflow-hidden mr-2">
<div className="relative mr-2 h-6 w-6 overflow-hidden rounded-full">
{post.primary_author.twitter ? (
<button
className="block cursor-pointer transition-opacity hover:opacity-90"
@@ -55,14 +55,20 @@ export function BlogPostCard({ post }: BlogPostCardProps) {
type="button"
>
<img
src={post.primary_author.profile_image}
src={
post.primary_author
.profile_image
}
alt={post.primary_author.name}
className="object-cover"
/>
</button>
) : (
<img
src={post.primary_author.profile_image}
src={
post.primary_author
.profile_image
}
alt={post.primary_author.name}
className="object-cover"
/>
@@ -71,18 +77,22 @@ export function BlogPostCard({ post }: BlogPostCardProps) {
)}
{post.primary_author?.twitter ? (
<button
className="hover:text-primary transition-colors"
className="transition-colors hover:text-primary"
onClick={handleTwitterClick}
type="button"
>
{post.primary_author.name || "Unknown Author"}
{post.primary_author.name ||
'Unknown Author'}
</button>
) : (
<span>{post.primary_author?.name || "Unknown Author"}</span>
<span>
{post.primary_author?.name ||
'Unknown Author'}
</span>
)}
</div>
<span className="mx-2">in</span>
<span>{post.primary_tag?.name || "General"}</span>
<span>{post.primary_tag?.name || 'General'}</span>
<span className="mx-2"></span>
<span>{post.reading_time} min read</span>
<span className="mx-2"></span>
@@ -91,5 +101,5 @@ export function BlogPostCard({ post }: BlogPostCardProps) {
</div>
</article>
</Link>
);
)
}

View File

@@ -1,4 +1,4 @@
"use client";
'use client'
import {
Select,
@@ -6,27 +6,27 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useDebounce } from "@/lib/hooks/use-debounce";
import { Search } from "lucide-react";
import { useRouter } from "next/navigation";
import { useCallback, useTransition } from "react";
} from '@/components/ui/select'
import { useDebounce } from '@/lib/hooks/use-debounce'
import { Search } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { useCallback, useTransition } from 'react'
interface Tag {
id: string;
name: string;
slug: string;
id: string
name: string
slug: string
}
interface SearchAndFilterProps {
tags: Tag[];
initialSearch: string;
initialTag: string;
searchPlaceholder: string;
allTagsText: string;
tags: Tag[]
initialSearch: string
initialTag: string
searchPlaceholder: string
allTagsText: string
}
const ALL_TAGS_VALUE = "all";
const ALL_TAGS_VALUE = 'all'
export function SearchAndFilter({
tags,
@@ -35,44 +35,44 @@ export function SearchAndFilter({
searchPlaceholder,
allTagsText,
}: SearchAndFilterProps) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const router = useRouter()
const [isPending, startTransition] = useTransition()
const handleTagChange = (value: string) => {
const searchParams = new URLSearchParams(window.location.search);
const searchParams = new URLSearchParams(window.location.search)
if (value && value !== ALL_TAGS_VALUE) {
searchParams.set("tag", value);
searchParams.set('tag', value)
} else {
searchParams.delete("tag");
searchParams.delete('tag')
}
startTransition(() => {
router.push(`?${searchParams.toString()}`);
});
};
router.push(`?${searchParams.toString()}`)
})
}
const debouncedCallback = useDebounce((value: string) => {
const searchParams = new URLSearchParams(window.location.search);
const searchParams = new URLSearchParams(window.location.search)
if (value) {
searchParams.set("search", value);
searchParams.set('search', value)
} else {
searchParams.delete("search");
searchParams.delete('search')
}
startTransition(() => {
router.push(`?${searchParams.toString()}`);
});
}, 300);
router.push(`?${searchParams.toString()}`)
})
}, 300)
const handleSearch = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
debouncedCallback(e.target.value);
debouncedCallback(e.target.value)
},
[debouncedCallback],
);
)
return (
<div className="flex flex-col md:flex-row gap-4 mb-8">
<div className="mb-8 flex flex-col gap-4 md:flex-row">
<div className="relative flex-1">
<div className="absolute inset-y-0 left-3 flex items-center pointer-events-none">
<div className="pointer-events-none absolute inset-y-0 left-3 flex items-center">
<Search className="h-5 w-5 text-gray-400" />
</div>
<input
@@ -80,7 +80,7 @@ export function SearchAndFilter({
defaultValue={initialSearch}
onChange={handleSearch}
placeholder={searchPlaceholder}
className="w-full pl-10 pr-4 py-2 border border-border rounded-md bg-background ring-offset-background placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
className="w-full rounded-md border border-border bg-background py-2 pl-10 pr-4 ring-offset-background placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
/>
</div>
<div className="w-full md:w-64">
@@ -92,7 +92,9 @@ export function SearchAndFilter({
<SelectValue placeholder={allTagsText} />
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL_TAGS_VALUE}>{allTagsText}</SelectItem>
<SelectItem value={ALL_TAGS_VALUE}>
{allTagsText}
</SelectItem>
{tags.map((tag) => (
<SelectItem key={tag.id} value={tag.slug}>
{tag.name}
@@ -102,5 +104,5 @@ export function SearchAndFilter({
</Select>
</div>
</div>
);
)
}

View File

@@ -1,55 +1,58 @@
import { getPosts, getTags } from "@/lib/ghost";
import type { Post } from "@/lib/ghost";
import { RssIcon } from "lucide-react";
import type { Metadata } from "next";
import Link from "next/link";
import { BlogPostCard } from "./components/BlogPostCard";
import { SearchAndFilter } from "./components/SearchAndFilter";
import { getPosts, getTags } from '@/lib/ghost'
import type { Post } from '@/lib/ghost'
import { RssIcon } from 'lucide-react'
import type { Metadata } from 'next'
import Link from 'next/link'
import { BlogPostCard } from './components/BlogPostCard'
import { SearchAndFilter } from './components/SearchAndFilter'
interface Tag {
id: string;
name: string;
slug: string;
id: string
name: string
slug: string
}
export const metadata: Metadata = {
title: "Blog",
description: "Latest news, updates, and articles from Dokploy",
};
title: 'Blog',
description: 'Latest news, updates, and articles from Dokploy',
}
export default async function BlogPage({
searchParams,
}: {
searchParams: { [key: string]: string | string[] | undefined };
searchParams: { [key: string]: string | string[] | undefined }
}) {
const searchParams2 = await searchParams;
const posts = await getPosts();
const tags = (await getTags()) as Tag[];
const searchParams2 = await searchParams
const posts = await getPosts()
const tags = (await getTags()) as Tag[]
const search =
typeof searchParams2.search === "string" ? searchParams2.search : "";
typeof searchParams2.search === 'string' ? searchParams2.search : ''
const selectedTag =
typeof searchParams2.tag === "string" ? searchParams2.tag : "";
typeof searchParams2.tag === 'string' ? searchParams2.tag : ''
const filteredPosts = posts.filter((post) => {
const matchesSearch =
search === "" ||
search === '' ||
post.title.toLowerCase().includes(search.toLowerCase()) ||
post.excerpt.toLowerCase().includes(search.toLowerCase());
post.excerpt.toLowerCase().includes(search.toLowerCase())
const matchesTag =
selectedTag === "" || post.tags?.some((tag) => tag.slug === selectedTag);
selectedTag === '' ||
post.tags?.some((tag) => tag.slug === selectedTag)
return matchesSearch && matchesTag;
});
return matchesSearch && matchesTag
})
return (
<div className="container mx-auto px-4 py-12 max-w-5xl">
<div className="flex items-center justify-between mb-8">
<div className="container mx-auto max-w-5xl px-4 py-12">
<div className="mb-8 flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground uppercase tracking-wider mb-2">
<p className="mb-2 text-sm uppercase tracking-wider text-muted-foreground">
BLOG
</p>
<h1 className="text-4xl font-bold">Dokploy Latest News & Updates</h1>
<h1 className="text-4xl font-bold">
Dokploy Latest News & Updates
</h1>
</div>
<Link
href="/rss.xml"
@@ -59,21 +62,23 @@ export default async function BlogPage({
</Link>
</div>
<SearchAndFilter
tags={tags}
initialSearch={search}
initialTag={selectedTag}
searchPlaceholder="Search posts..."
allTagsText="All Tags"
/>
<SearchAndFilter
tags={tags}
initialSearch={search}
initialTag={selectedTag}
searchPlaceholder="Search posts..."
allTagsText="All Tags"
/>
{filteredPosts.length === 0 ? (
<div className="text-center py-12 min-h-[20vh] flex items-center justify-center">
<p className="text-xl text-muted-foreground">
{search || selectedTag ? "No posts found matching your criteria" : "No posts available"}
</p>
</div>
) : (
{filteredPosts.length === 0 ? (
<div className="flex min-h-[20vh] items-center justify-center py-12 text-center">
<p className="text-xl text-muted-foreground">
{search || selectedTag
? 'No posts found matching your criteria'
: 'No posts available'}
</p>
</div>
) : (
<div className="space-y-8">
{filteredPosts.map((post: Post) => (
<BlogPostCard key={post.id} post={post} />
@@ -81,5 +86,5 @@ export default async function BlogPage({
</div>
)}
</div>
);
)
}

View File

@@ -1,59 +1,61 @@
import { getPostsByTag, getTags } from "@/lib/ghost";
import type { Post } from "@/lib/ghost";
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getPostsByTag, getTags } from '@/lib/ghost'
import type { Post } from '@/lib/ghost'
import type { Metadata } from 'next'
import Image from 'next/image'
import Link from 'next/link'
import { notFound } from 'next/navigation'
type Props = {
params: { tag: string };
};
params: { tag: string }
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { tag } = await params;
const posts = await getPostsByTag(tag);
const { tag } = await params
const posts = await getPostsByTag(tag)
if (!posts || posts.length === 0) {
return {
title: "Tag Not Found",
description: "The requested tag could not be found",
};
title: 'Tag Not Found',
description: 'The requested tag could not be found',
}
}
const tagName =
posts[0].tags?.find((t: { slug: string }) => t.slug === tag)?.name || tag;
posts[0].tags?.find((t: { slug: string }) => t.slug === tag)?.name ||
tag
return {
title: `${tagName} Posts`,
description: `Browse all posts tagged with ${tagName}`,
};
}
}
export async function generateStaticParams() {
const tags = await getTags();
return tags.map((tag: { slug: string }) => ({ tag: tag.slug }));
const tags = await getTags()
return tags.map((tag: { slug: string }) => ({ tag: tag.slug }))
}
export default async function TagPage({ params }: Props) {
const { tag } = await params;
const posts = await getPostsByTag(tag);
const { tag } = await params
const posts = await getPostsByTag(tag)
if (!posts || posts.length === 0) {
notFound();
notFound()
}
const tagName =
posts[0].tags?.find((t: { slug: string }) => t.slug === tag)?.name || tag;
posts[0].tags?.find((t: { slug: string }) => t.slug === tag)?.name ||
tag
return (
<div className="container mx-auto px-4 py-12">
<Link
href="/blog"
className="inline-flex items-center mb-8 text-primary-600 hover:text-primary-800 transition-colors"
className="text-primary-600 hover:text-primary-800 mb-8 inline-flex items-center transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 mr-2"
className="mr-2 h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
@@ -67,8 +69,8 @@ export default async function TagPage({ params }: Props) {
</Link>
<div className="mb-8">
<h1 className="text-3xl font-bold mb-2">
Posts tagged with{" "}
<h1 className="mb-2 text-3xl font-bold">
Posts tagged with{' '}
<span className="text-primary-600">"{tagName}"</span>
</h1>
<p className="text-gray-600 dark:text-gray-400">
@@ -76,25 +78,25 @@ export default async function TagPage({ params }: Props) {
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
{posts.map((post: Post) => (
<BlogPostCard key={post.id} post={post} />
))}
</div>
</div>
);
)
}
function BlogPostCard({ post }: { post: Post }) {
const formattedDate = new Date(post.published_at).toLocaleDateString("en", {
year: "numeric",
month: "long",
day: "numeric",
});
const formattedDate = new Date(post.published_at).toLocaleDateString('en', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
return (
<Link href={`/blog/${post.slug}`} className="group">
<div className="dark:bg-gray-800 rounded-lg overflow-hidden shadow-lg transition-all duration-300 hover:shadow-xl">
<div className="overflow-hidden rounded-lg shadow-lg transition-all duration-300 hover:shadow-xl dark:bg-gray-800">
{post.feature_image && (
<div className="relative h-48 w-full">
<Image
@@ -106,18 +108,18 @@ function BlogPostCard({ post }: { post: Post }) {
</div>
)}
<div className="p-6">
<h2 className="text-xl font-semibold mb-2 group-hover:text-primary-500 transition-colors">
<h2 className="group-hover:text-primary-500 mb-2 text-xl font-semibold transition-colors">
{post.title}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
<p className="mb-4 text-sm text-gray-500 dark:text-gray-400">
{formattedDate} {post.reading_time} min read
</p>
<p className="text-gray-700 dark:text-gray-300 mb-4">
<p className="mb-4 text-gray-700 dark:text-gray-300">
{post.custom_excerpt || post.excerpt}
</p>
<div className="flex items-center">
{post.primary_author?.profile_image && (
<div className="relative h-10 w-10 rounded-full overflow-hidden mr-3">
<div className="relative mr-3 h-10 w-10 overflow-hidden rounded-full">
<Image
src={post.primary_author.profile_image}
alt={post.primary_author.name}
@@ -128,12 +130,12 @@ function BlogPostCard({ post }: { post: Post }) {
)}
<div>
<p className="font-medium">
{post.primary_author?.name || "Unknown Author"}
{post.primary_author?.name || 'Unknown Author'}
</p>
</div>
</div>
</div>
</div>
</Link>
);
)
}

File diff suppressed because one or more lines are too long

View File

@@ -1,13 +1,12 @@
import type { Metadata } from "next";
import type { ReactNode } from "react";
import type { Metadata } from 'next'
import type { ReactNode } from 'react'
export const metadata: Metadata = {
title: "Contact Us",
title: 'Contact Us',
description:
"Get in touch with our team. We're here to help with any questions about Dokploy.",
};
export default function ContactLayout({ children }: { children: ReactNode }) {
return <>{children}</>;
}
export default function ContactLayout({ children }: { children: ReactNode }) {
return <>{children}</>
}

View File

@@ -1,126 +1,126 @@
"use client";
'use client'
import { useState } from "react";
import { Container } from "@/components/Container";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useState } from 'react'
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";
} 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";
firstName: string;
lastName: string;
email: string;
company: string;
message: string;
inquiryType: '' | 'support' | 'sales' | 'other'
firstName: string
lastName: string
email: string
company: string
message: string
}
export default function ContactPage() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false)
const [isSubmitted, setIsSubmitted] = useState(false)
const [formData, setFormData] = useState<ContactFormData>({
inquiryType: "",
firstName: "",
lastName: "",
email: "",
company: "",
message: "",
});
const [errors, setErrors] = useState<Record<string, string>>({});
inquiryType: '',
firstName: '',
lastName: '',
email: '',
company: '',
message: '',
})
const [errors, setErrors] = useState<Record<string, string>>({})
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
const newErrors: Record<string, string> = {}
if (!formData.inquiryType) {
newErrors.inquiryType = "Please select what we can help you with";
newErrors.inquiryType = 'Please select what we can help you with'
}
if (!formData.firstName.trim()) {
newErrors.firstName = "First name is required";
newErrors.firstName = 'First name is required'
}
if (!formData.lastName.trim()) {
newErrors.lastName = "Last name is required";
newErrors.lastName = 'Last name is required'
}
if (!formData.email.trim()) {
newErrors.email = "Email is required";
newErrors.email = 'Email is required'
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = "Please enter a valid email address";
newErrors.email = 'Please enter a valid email address'
}
if (!formData.company.trim()) {
newErrors.company = "Company name is required";
newErrors.company = 'Company name is required'
}
if (!formData.message.trim()) {
newErrors.message = "Message is required";
newErrors.message = 'Message is required'
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
e.preventDefault()
if (!validateForm()) {
return;
return
}
setIsSubmitting(true);
setIsSubmitting(true)
try {
const response = await fetch("/api/contact", {
method: "POST",
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
})
if (response.ok) {
trackGAEvent({
action: "Contact Form Submitted",
category: "Contact",
action: 'Contact Form Submitted',
category: 'Contact',
label: formData.inquiryType,
});
})
setFormData({
inquiryType: "",
firstName: "",
lastName: "",
email: "",
company: "",
message: "",
});
setErrors({});
setIsSubmitted(true);
inquiryType: '',
firstName: '',
lastName: '',
email: '',
company: '',
message: '',
})
setErrors({})
setIsSubmitted(true)
} else {
throw new Error("Failed to submit form");
throw new Error('Failed to submit form')
}
} catch (error) {
console.error("Error submitting form:", error);
alert("There was an error sending your message. Please try again.");
console.error('Error submitting form:', error)
alert('There was an error sending your message. Please try again.')
} finally {
setIsSubmitting(false);
setIsSubmitting(false)
}
};
}
const handleInputChange = (field: keyof ContactFormData, value: any) => {
setFormData((prev) => ({ ...prev, [field]: value }));
setFormData((prev) => ({ ...prev, [field]: value }))
if (errors[field]) {
setErrors((prev) => {
const newErrors = { ...prev };
delete newErrors[field];
return newErrors;
});
const newErrors = { ...prev }
delete newErrors[field]
return newErrors
})
}
};
}
if (isSubmitted) {
return (
@@ -131,22 +131,25 @@ export default function ContactPage() {
Thank you for contacting us!
</h1>
<p className="mt-6 text-lg leading-8 text-muted-foreground">
We've received your message and will get back to you as soon as
possible.
We've received your message and will get back to you
as soon as possible.
</p>
<div className="mt-10">
<Button onClick={() => setIsSubmitted(false)} variant="outline">
<Button
onClick={() => setIsSubmitted(false)}
variant="outline"
>
Send Another Message
</Button>
</div>
</div>
</Container>
</div>
);
)
}
return (
<div className="bg-background py-24 sm:py-32 relative">
<div className="relative bg-background py-24 sm:py-32">
<AnimatedGridPattern
numSquares={30}
maxOpacity={0.1}
@@ -155,19 +158,19 @@ export default function ContactPage() {
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",
'[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="relative z-10 mx-auto max-w-3xl rounded-lg border border-border bg-black p-8">
<div className="text-center">
<h1 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
Contact Us
</h1>
<p className="mt-6 text-lg leading-8 text-muted-foreground">
Get in touch with our team. We're here to help with any questions
about Dokploy.
Get in touch with our team. We're here to help with
any questions about Dokploy.
</p>
</div>
@@ -177,15 +180,15 @@ export default function ContactPage() {
htmlFor="inquiryType"
className="block text-sm font-medium text-foreground"
>
What can we help you with today?{" "}
What can we help you with today?{' '}
<span className="text-red-500">*</span>
</label>
<Select
value={formData.inquiryType}
onValueChange={(value) =>
handleInputChange(
"inquiryType",
value as "support" | "sales" | "other",
'inquiryType',
value as 'support' | 'sales' | 'other',
)
}
>
@@ -193,13 +196,17 @@ export default function ContactPage() {
<SelectValue placeholder="Select an option" />
</SelectTrigger>
<SelectContent>
<SelectItem value="support">Support</SelectItem>
<SelectItem value="support">
Support
</SelectItem>
<SelectItem value="sales">Sales</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
{errors.inquiryType && (
<p className="text-sm text-red-600">{errors.inquiryType}</p>
<p className="text-sm text-red-600">
{errors.inquiryType}
</p>
)}
</div>
@@ -209,19 +216,25 @@ export default function ContactPage() {
htmlFor="firstName"
className="block text-sm font-medium text-foreground"
>
First Name <span className="text-red-500">*</span>
First Name{' '}
<span className="text-red-500">*</span>
</label>
<Input
id="firstName"
type="text"
value={formData.firstName}
onChange={(e) =>
handleInputChange("firstName", e.target.value)
handleInputChange(
'firstName',
e.target.value,
)
}
placeholder="Your first name"
/>
{errors.firstName && (
<p className="text-sm text-red-600">{errors.firstName}</p>
<p className="text-sm text-red-600">
{errors.firstName}
</p>
)}
</div>
@@ -230,19 +243,25 @@ export default function ContactPage() {
htmlFor="lastName"
className="block text-sm font-medium text-foreground"
>
Last Name <span className="text-red-500">*</span>
Last Name{' '}
<span className="text-red-500">*</span>
</label>
<Input
id="lastName"
type="text"
value={formData.lastName}
onChange={(e) =>
handleInputChange("lastName", e.target.value)
handleInputChange(
'lastName',
e.target.value,
)
}
placeholder="Your last name"
/>
{errors.lastName && (
<p className="text-sm text-red-600">{errors.lastName}</p>
<p className="text-sm text-red-600">
{errors.lastName}
</p>
)}
</div>
</div>
@@ -258,11 +277,15 @@ export default function ContactPage() {
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
onChange={(e) =>
handleInputChange('email', e.target.value)
}
placeholder="your.email@company.com"
/>
{errors.email && (
<p className="text-sm text-red-600">{errors.email}</p>
<p className="text-sm text-red-600">
{errors.email}
</p>
)}
</div>
@@ -271,17 +294,22 @@ export default function ContactPage() {
htmlFor="company"
className="block text-sm font-medium text-foreground"
>
Company Name <span className="text-red-500">*</span>
Company Name{' '}
<span className="text-red-500">*</span>
</label>
<Input
id="company"
type="text"
value={formData.company}
onChange={(e) => handleInputChange("company", e.target.value)}
onChange={(e) =>
handleInputChange('company', e.target.value)
}
placeholder="Your company name"
/>
{errors.company && (
<p className="text-sm text-red-600">{errors.company}</p>
<p className="text-sm text-red-600">
{errors.company}
</p>
)}
</div>
@@ -290,18 +318,23 @@ export default function ContactPage() {
htmlFor="message"
className="block text-sm font-medium text-foreground"
>
How can we help? <span className="text-red-500">*</span>
How can we help?{' '}
<span className="text-red-500">*</span>
</label>
<textarea
id="message"
value={formData.message}
onChange={(e) => handleInputChange("message", e.target.value)}
onChange={(e) =>
handleInputChange('message', e.target.value)
}
placeholder="Tell us more about your inquiry..."
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"
className="flex w-full resize-none rounded-md border border-input bg-background bg-input 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"
/>
{errors.message && (
<p className="text-sm text-red-600">{errors.message}</p>
<p className="text-sm text-red-600">
{errors.message}
</p>
)}
</div>
@@ -311,12 +344,12 @@ export default function ContactPage() {
disabled={isSubmitting}
className="min-w-[120px]"
>
{isSubmitting ? "Sending..." : "Send Message"}
{isSubmitting ? 'Sending...' : 'Send Message'}
</Button>
</div>
</form>
</div>
</Container>
</div>
);
)
}

View File

@@ -1,52 +1,52 @@
import clsx from "clsx";
import type { Metadata } from "next";
import { Inter, Lexend } from "next/font/google";
import type { ReactNode } from "react";
import { GoogleAnalytics } from "@next/third-parties/google";
import "@/styles/tailwind.css";
import "react-photo-view/dist/react-photo-view.css";
import { Header } from "@/components/Header";
import { Footer } from "@/components/Footer";
import clsx from 'clsx'
import type { Metadata } from 'next'
import { Inter, Lexend } from 'next/font/google'
import type { ReactNode } from 'react'
import { GoogleAnalytics } from '@next/third-parties/google'
import '@/styles/tailwind.css'
import 'react-photo-view/dist/react-photo-view.css'
import { Header } from '@/components/Header'
import { Footer } from '@/components/Footer'
type Props = {
children: ReactNode;
};
children: ReactNode
}
export const metadata: Metadata = {
metadataBase: new URL("https://dokploy.com"),
metadataBase: new URL('https://dokploy.com'),
title: {
default: "Dokploy - Deploy your applications with ease",
template: "%s | Dokploy",
default: 'Dokploy - Deploy your applications with ease',
template: '%s | Dokploy',
},
description: "Deploy your applications with ease using Dokploy",
description: 'Deploy your applications with ease using Dokploy',
icons: {
icon: "icon.svg",
apple: "apple-touch-icon.png",
icon: 'icon.svg',
apple: 'apple-touch-icon.png',
},
openGraph: {
title: "Dokploy - Deploy your applications with ease",
description: "Deploy your applications with ease using Dokploy",
images: "/og.png",
type: "website",
title: 'Dokploy - Deploy your applications with ease',
description: 'Deploy your applications with ease using Dokploy',
images: '/og.png',
type: 'website',
},
twitter: {
card: "summary_large_image",
title: "Dokploy - Deploy your applications with ease",
description: "Deploy your applications with ease using Dokploy",
images: ["/og.png"],
card: 'summary_large_image',
title: 'Dokploy - Deploy your applications with ease',
description: 'Deploy your applications with ease using Dokploy',
images: ['/og.png'],
},
};
}
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-inter",
});
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
})
const lexend = Lexend({
subsets: ["latin"],
display: "swap",
variable: "--font-lexend",
});
subsets: ['latin'],
display: 'swap',
variable: '--font-lexend',
})
// Since we have a `not-found.tsx` page on the root, a layout file
// is required, even if it's just passing children through.
export default function RootLayout({ children }: { children: ReactNode }) {
@@ -54,7 +54,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
<html
lang="en"
className={clsx(
"h-full scroll-smooth antialiased",
'h-full scroll-smooth antialiased',
inter.variable,
lexend.variable,
)}
@@ -77,5 +77,5 @@ export default function RootLayout({ children }: { children: ReactNode }) {
</div>
</body>
</html>
);
)
}

View File

@@ -1,6 +1,6 @@
"use client";
'use client'
import NextError from "next/error";
import NextError from 'next/error'
export default function NotFound() {
return (
@@ -9,5 +9,5 @@ export default function NotFound() {
<NextError statusCode={404} />
</body>
</html>
);
)
}

View File

@@ -1,20 +1,21 @@
import { CallToAction } from "@/components/CallToAction";
import { Faqs } from "@/components/Faqs";
import { Hero } from "@/components/Hero";
import { Testimonials } from "@/components/Testimonials";
import { FirstFeaturesSection } from "@/components/first-features";
import { Pricing } from "@/components/pricing";
import { SecondaryFeaturesSections } from "@/components/secondary-features";
import { Sponsors } from "@/components/sponsors";
import { StatsSection } from "@/components/stats";
import type { Metadata } from "next";
import { CallToAction } from '@/components/CallToAction'
import { Faqs } from '@/components/Faqs'
import { Hero } from '@/components/Hero'
import { Testimonials } from '@/components/Testimonials'
import { FirstFeaturesSection } from '@/components/first-features'
import { Pricing } from '@/components/pricing'
import { SecondaryFeaturesSections } from '@/components/secondary-features'
import { Sponsors } from '@/components/sponsors'
import { StatsSection } from '@/components/stats'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
absolute: "Dokploy - Deploy your applications with ease",
absolute: 'Dokploy - Deploy your applications with ease',
},
description: "Open-source self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases",
};
description:
'Open-source self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases',
}
export default function Home() {
return (
@@ -25,7 +26,7 @@ export default function Home() {
<SecondaryFeaturesSections />
<StatsSection />
<Testimonials />
<div className="w-full relative">
<div className="relative w-full">
<Pricing />
</div>
<Faqs />
@@ -33,5 +34,5 @@ export default function Home() {
<CallToAction />
</main>
</div>
);
)
}

View File

@@ -1,37 +1,43 @@
import type { Metadata } from "next";
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: "Privacy Policy",
title: 'Privacy Policy',
description:
"Learn about how Dokploy collects, uses, and safeguards your personal information when you use our website and services.",
};
'Learn about how Dokploy collects, uses, and safeguards your personal information when you use our website and services.',
}
export default function PrivacyPage() {
return (
<div className="flex flex-col gap-4 w-full max-w-4xl mx-auto py-12 px-4">
<h1 className="text-3xl font-bold text-center mb-6">Privacy</h1>
<div className="mx-auto flex w-full max-w-4xl flex-col gap-4 px-4 py-12">
<h1 className="mb-6 text-center text-3xl font-bold">Privacy</h1>
<section className="flex flex-col gap-2">
<p>
At Dokploy, we are committed to protecting your privacy. This Privacy
Policy explains how we collect, use, and safeguard your personal
information when you use our website and services.
At Dokploy, we are committed to protecting your privacy.
This Privacy Policy explains how we collect, use, and
safeguard your personal information when you use our website
and services.
</p>
<p>
By using Dokploy, you agree to the collection and use of information
in accordance with this Privacy Policy. If you do not agree with these
practices, please do not use our services.
By using Dokploy, you agree to the collection and use of
information in accordance with this Privacy Policy. If you
do not agree with these practices, please do not use our
services.
</p>
<h2 className="text-2xl font-semibold mb-4">
<h2 className="mb-4 text-2xl font-semibold">
1. Information We Collect
</h2>
<p className="">
We only collect limited, non-personal data through Umami Analytics, a
privacy-focused analytics tool. No personal identifying information
(PII) is collected. The data we collect includes:
We only collect limited, non-personal data through Umami
Analytics, a privacy-focused analytics tool. No personal
identifying information (PII) is collected. The data we
collect includes:
</p>
<ul className="list-disc list-inside mb-4">
<li>Website usage statistics (e.g., page views, session duration)</li>
<ul className="mb-4 list-inside list-disc">
<li>
Website usage statistics (e.g., page views, session
duration)
</li>
<li>Anonymized IP addresses</li>
<li>Referring websites</li>
<li>Browser and device type</li>
@@ -39,73 +45,80 @@ export default function PrivacyPage() {
</section>
<section className="">
<h2 className="text-2xl font-semibold mb-4">
<h2 className="mb-4 text-2xl font-semibold">
2. How We Use the Information
</h2>
<p className="mb-4">
The information we collect is used solely for improving the
functionality and user experience of our platform. Specifically, we
use it to:
functionality and user experience of our platform.
Specifically, we use it to:
</p>
<ul className="list-disc list-inside mb-4">
<ul className="mb-4 list-inside list-disc">
<li>Monitor traffic and website performance</li>
<li>Optimize the user experience</li>
<li>Understand how users interact with our platform</li>
</ul>
<p>
Additionally, we use a single cookie to manage user sessions, which is
necessary for the proper functioning of the platform.
Additionally, we use a single cookie to manage user
sessions, which is necessary for the proper functioning of
the platform.
</p>
</section>
<section className="flex flex-col gap-2">
<h2 className="text-2xl font-semibold mb-4">3. Data Security</h2>
<h2 className="mb-4 text-2xl font-semibold">
3. Data Security
</h2>
<p className="">
We take reasonable precautions to protect your data. Since we do not
collect personal information, the risk of data misuse is minimized.
Umami Analytics is privacy-friendly and does not rely on cookies or
store PII.
We take reasonable precautions to protect your data. Since
we do not collect personal information, the risk of data
misuse is minimized. Umami Analytics is privacy-friendly and
does not rely on cookies or store PII.
</p>
</section>
<section className="">
<h2 className="text-2xl font-semibold mb-4">4. Third-Party Services</h2>
<h2 className="mb-4 text-2xl font-semibold">
4. Third-Party Services
</h2>
<p>
We do not share your data with any third-party services other than
Umami Analytics. We do not sell, trade, or transfer your data to
outside parties.
We do not share your data with any third-party services
other than Umami Analytics. We do not sell, trade, or
transfer your data to outside parties.
</p>
</section>
<section className="">
<h2 className="text-2xl font-semibold mb-4">5. Cookies</h2>
<h2 className="mb-4 text-2xl font-semibold">5. Cookies</h2>
<p className="mb-4">
Dokploy does not use cookies to track user activity. Umami Analytics
is cookie-free and does not require any tracking cookies for its
functionality.
Dokploy does not use cookies to track user activity. Umami
Analytics is cookie-free and does not require any tracking
cookies for its functionality.
</p>
</section>
<section className="flex flex-col gap-2">
<h2 className="text-2xl font-semibold mb-4">
<h2 className="mb-4 text-2xl font-semibold">
6. Changes to This Privacy Policy
</h2>
<p className="">
We may update this Privacy Policy from time to time. Any changes will
be posted on this page, and it is your responsibility to review this
policy periodically.
We may update this Privacy Policy from time to time. Any
changes will be posted on this page, and it is your
responsibility to review this policy periodically.
</p>
</section>
<section className="">
<h2 className="text-2xl font-semibold mb-4">12. Contact Information</h2>
<h2 className="mb-4 text-2xl font-semibold">
12. Contact Information
</h2>
<p className="mb-4">
If you have any questions or concerns regarding these Privacy Policy,
please contact us at:
If you have any questions or concerns regarding these
Privacy Policy, please contact us at:
</p>
<p className="mb-4">
Email:{" "}
Email:{' '}
<a
href="mailto:support@dokploy.com"
className="text-blue-500 hover:underline"
@@ -115,6 +128,5 @@ export default function PrivacyPage() {
</p>
</section>
</div>
);
)
}

View File

@@ -1,11 +1,11 @@
import type { MetadataRoute } from "next";
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
userAgent: '*',
allow: '/',
},
sitemap: "https://dokploy.com/sitemap.xml",
};
sitemap: 'https://dokploy.com/sitemap.xml',
}
}

View File

@@ -1,34 +1,34 @@
import { getPosts } from "@/lib/ghost";
import { NextResponse } from "next/server";
import { getPosts } from '@/lib/ghost'
import { NextResponse } from 'next/server'
function escapeXml(unsafe: string): string {
return unsafe.replace(/[<>&'"]/g, (c) => {
switch (c) {
case "<":
return "&lt;";
case ">":
return "&gt;";
case "&":
return "&amp;";
case '<':
return '&lt;'
case '>':
return '&gt;'
case '&':
return '&amp;'
case "'":
return "&apos;";
return '&apos;'
case '"':
return "&quot;";
return '&quot;'
default:
return c;
return c
}
});
})
}
export async function GET() {
const posts = await getPosts();
const posts = await getPosts()
const rss = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>Dokploy Blog</title>
<link>https://dokploy.com/blog</link>
<description>${escapeXml("Dokploy Latest News & Updates")}</description>
<description>${escapeXml('Dokploy Latest News & Updates')}</description>
<language>en</language>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
${posts
@@ -44,23 +44,23 @@ export async function GET() {
${
post.feature_image
? `<enclosure url="${escapeXml(post.feature_image)}" type="image/jpeg" />`
: ""
: ''
}
${
post.primary_author
? `<dc:creator><![CDATA[${post.primary_author.name}]]></dc:creator>`
: ""
: ''
}
</item>`,
)
.join("\n")}
.join('\n')}
</channel>
</rss>`;
</rss>`
return new NextResponse(rss, {
headers: {
"Content-Type": "application/xml; charset=utf-8",
"Cache-Control": "s-maxage=3600, stale-while-revalidate",
'Content-Type': 'application/xml; charset=utf-8',
'Cache-Control': 's-maxage=3600, stale-while-revalidate',
},
});
})
}

View File

@@ -1,26 +1,26 @@
import { getPosts } from "@/lib/ghost";
import type { MetadataRoute } from "next";
import { getPosts } from '@/lib/ghost'
import type { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPosts();
const posts = await getPosts()
return [
{
url: "https://dokploy.com",
url: 'https://dokploy.com',
lastModified: new Date(),
changeFrequency: "monthly",
changeFrequency: 'monthly',
priority: 1,
},
{
url: "https://dokploy.com/blog",
url: 'https://dokploy.com/blog',
lastModified: new Date(),
changeFrequency: "monthly",
changeFrequency: 'monthly',
priority: 0.8,
},
...posts.map((post) => ({
url: `https://dokploy.com/blog/${post.slug}`,
lastModified: new Date(post.published_at),
changeFrequency: "monthly" as const,
changeFrequency: 'monthly' as const,
priority: 0.8,
})),
];
]
}

View File

@@ -1,29 +1,30 @@
import type { Metadata } from "next";
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: "Terms and Conditions",
title: 'Terms and Conditions',
description:
"Read the terms and conditions for using Dokploy's website and services.",
};
}
export default function TermsPage() {
return (
<div className="flex flex-col gap-4 w-full max-w-4xl mx-auto py-12 px-4">
<h1 className="text-3xl font-bold text-center mb-6">
<div className="mx-auto flex w-full max-w-4xl flex-col gap-4 px-4 py-12">
<h1 className="mb-6 text-center text-3xl font-bold">
Terms and Conditions
</h1>
<section className="flex flex-col gap-2">
<p>
Welcome to Dokploy! These Terms and Conditions outline the rules and
regulations for the use of Dokploy's website and services.
Welcome to Dokploy! These Terms and Conditions outline the
rules and regulations for the use of Dokploy's website and
services.
</p>
<p>
By accessing or using our services, you agree to be bound by the
following terms. If you do not agree with these terms, please do not
use our website or services.
By accessing or using our services, you agree to be bound by
the following terms. If you do not agree with these terms,
please do not use our website or services.
</p>
<h2 className="text-2xl font-semibold mb-4">1. Definitions</h2>
<h2 className="mb-4 text-2xl font-semibold">1. Definitions</h2>
<p className="">
Website: Refers to the website of Dokploy (
<a
@@ -35,171 +36,189 @@ export default function TermsPage() {
) and its subdomains.
</p>
<p>
Services: The platform and related services offered by Dokploy for
deploying and managing applications using Docker and other related
tools.
Services: The platform and related services offered by
Dokploy for deploying and managing applications using Docker
and other related tools.
</p>
<p>User: Any individual or organization using Dokploy.</p>
<p>
Subscription: The paid plan for using additional features, resources,
or server capacity.
Subscription: The paid plan for using additional features,
resources, or server capacity.
</p>
</section>
<section className="">
<h2 className="text-2xl font-semibold mb-4">2. Service Description</h2>
<h2 className="mb-4 text-2xl font-semibold">
2. Service Description
</h2>
<p className="mb-4">
Dokploy is a platform that allows users to deploy and manage web
applications on their own servers using custom builders and Docker
technology. Dokploy offers both free and paid services, including
subscriptions for adding additional servers, features, or increased
capacity.
Dokploy is a platform that allows users to deploy and manage
web applications on their own servers using custom builders
and Docker technology. Dokploy offers both free and paid
services, including subscriptions for adding additional
servers, features, or increased capacity.
</p>
</section>
<section className="flex flex-col gap-2">
<h2 className="text-2xl font-semibold mb-4">
<h2 className="mb-4 text-2xl font-semibold">
3. User Responsibilities
</h2>
<p className="">
Users are responsible for maintaining the security of their accounts,
servers, and applications deployed through Dokploy.
Users are responsible for maintaining the security of their
accounts, servers, and applications deployed through
Dokploy.
</p>
<p className="">
Users must not use the platform for illegal activities, including but
not limited to distributing malware, violating intellectual property
rights, or engaging in cyberattacks.
Users must not use the platform for illegal activities,
including but not limited to distributing malware, violating
intellectual property rights, or engaging in cyberattacks.
</p>
<p className="">
Users must comply with all local, state, and international laws in
connection with their use of Dokploy.
Users must comply with all local, state, and international
laws in connection with their use of Dokploy.
</p>
</section>
<section className="">
<h2 className="text-2xl font-semibold mb-4">
<h2 className="mb-4 text-2xl font-semibold">
4. Subscription and Payment
</h2>
<ul className="list-disc list-inside mb-4">
<ul className="mb-4 list-inside list-disc">
<li>
By purchasing a subscription, users agree to the pricing and payment
terms detailed on the website or via Paddle (our payment processor).
By purchasing a subscription, users agree to the pricing
and payment terms detailed on the website or via Paddle
(our payment processor).
</li>
<li>
Subscriptions renew automatically unless canceled by the user before
the renewal date.
Subscriptions renew automatically unless canceled by the
user before the renewal date.
</li>
</ul>
</section>
<section className="">
<h2 className="text-2xl font-semibold mb-4">5. Refund Policy</h2>
<h2 className="mb-4 text-2xl font-semibold">
5. Refund Policy
</h2>
<p className="mb-4">
Due to the nature of our digital services, Dokploy operates on a
no-refund policy for any paid subscriptions, except where required by
law. We offer a self-hosted version of Dokploy with the same core
functionalities, which users can deploy and use without any cost. We
recommend users try the self-hosted version to evaluate the platform
before committing to a paid subscription.
Due to the nature of our digital services, Dokploy operates
on a no-refund policy for any paid subscriptions, except
where required by law. We offer a self-hosted version of
Dokploy with the same core functionalities, which users can
deploy and use without any cost. We recommend users try the
self-hosted version to evaluate the platform before
committing to a paid subscription.
</p>
</section>
<section className="flex flex-col gap-2">
<h2 className="text-2xl font-semibold mb-4">
<h2 className="mb-4 text-2xl font-semibold">
6. Limitations of Liability
</h2>
<p className="">
Dokploy is provided "as is" without any warranties, express or
implied, including but not limited to the availability, reliability,
or accuracy of the service.
Dokploy is provided "as is" without any warranties, express
or implied, including but not limited to the availability,
reliability, or accuracy of the service.
</p>
<p className="">
Users are fully responsible for any modifications made to their remote
servers or the environment where Dokploy is deployed. Any changes to
the server configuration, system settings, security policies, or other
environments that deviate from the recommended use of Dokploy may
result in compatibility issues, performance degradation, or security
vulnerabilities. Additionally, Dokploy may not function properly on
unsupported operating systems or environments. We do not guarantee the
platform will operate correctly or reliably under modified server
conditions or on unsupported systems, and we will not be held liable
for any disruptions, malfunctions, or damages resulting from such
changes or unsupported configurations.
Users are fully responsible for any modifications made to
their remote servers or the environment where Dokploy is
deployed. Any changes to the server configuration, system
settings, security policies, or other environments that
deviate from the recommended use of Dokploy may result in
compatibility issues, performance degradation, or security
vulnerabilities. Additionally, Dokploy may not function
properly on unsupported operating systems or environments.
We do not guarantee the platform will operate correctly or
reliably under modified server conditions or on unsupported
systems, and we will not be held liable for any disruptions,
malfunctions, or damages resulting from such changes or
unsupported configurations.
</p>
</section>
<section className="">
<h2 className="text-2xl font-semibold mb-4">
<h2 className="mb-4 text-2xl font-semibold">
7. Service Modifications and Downtime
</h2>
<p className="mb-4">
While we strive to provide uninterrupted service, there may be periods
of downtime due to scheduled maintenance or upgrades to our
infrastructure, such as server maintenance or system improvements. We
will provide notice to users ahead of any planned maintenance.
While we strive to provide uninterrupted service, there may
be periods of downtime due to scheduled maintenance or
upgrades to our infrastructure, such as server maintenance
or system improvements. We will provide notice to users
ahead of any planned maintenance.
</p>
</section>
<section className="flex flex-col gap-2">
<h2 className="text-2xl font-semibold mb-4">
<h2 className="mb-4 text-2xl font-semibold">
8. Intellectual Property
</h2>
<p className="">
Dokploy retains all intellectual property rights to the platform,
including code, design, and content.
Dokploy retains all intellectual property rights to the
platform, including code, design, and content.
</p>
<p className="">
Users are granted a limited, non-exclusive, and non-transferable
license to use Dokploy in accordance with these terms.
Users are granted a limited, non-exclusive, and
non-transferable license to use Dokploy in accordance with
these terms.
</p>
<p className="">
Users may not modify, reverse-engineer, or distribute any part of the
platform without express permission.
Users may not modify, reverse-engineer, or distribute any
part of the platform without express permission.
</p>
</section>
<section className="">
<h2 className="text-2xl font-semibold mb-4">9. Termination</h2>
<h2 className="mb-4 text-2xl font-semibold">9. Termination</h2>
<p className="mb-4">
Dokploy reserves the right to suspend or terminate access to the
platform for users who violate these terms or engage in harmful
behavior.
Dokploy reserves the right to suspend or terminate access to
the platform for users who violate these terms or engage in
harmful behavior.
</p>
<p className="mb-4">
Users may terminate their account at any time by contacting support.
Upon termination, access to the platform will be revoked, and any
stored data may be permanently deleted.
Users may terminate their account at any time by contacting
support. Upon termination, access to the platform will be
revoked, and any stored data may be permanently deleted.
</p>
</section>
<section className="">
<h2 className="text-2xl font-semibold mb-4">10. Changes to Terms</h2>
<h2 className="mb-4 text-2xl font-semibold">
10. Changes to Terms
</h2>
<p className="mb-4">
Dokploy reserves the right to update these Terms & Conditions at any
time. Changes will be effective immediately upon posting on the
website. It is the user's responsibility to review these terms
periodically.
Dokploy reserves the right to update these Terms &
Conditions at any time. Changes will be effective
immediately upon posting on the website. It is the user's
responsibility to review these terms periodically.
</p>
</section>
<section className="">
<h2 className="text-2xl font-semibold mb-4">11. Governing Law</h2>
<h2 className="mb-4 text-2xl font-semibold">
11. Governing Law
</h2>
<p className="mb-4">
These Terms & Conditions are governed by applicable laws based on the
user's location. Any disputes arising under these terms will be
resolved in accordance with the legal jurisdiction relevant to the
user's location, unless otherwise required by applicable law.
These Terms & Conditions are governed by applicable laws
based on the user's location. Any disputes arising under
these terms will be resolved in accordance with the legal
jurisdiction relevant to the user's location, unless
otherwise required by applicable law.
</p>
</section>
<section className="">
<h2 className="text-2xl font-semibold mb-4">12. Contact Information</h2>
<h2 className="mb-4 text-2xl font-semibold">
12. Contact Information
</h2>
<p className="mb-4">
If you have any questions or concerns regarding these Terms, you can
reach us at:
If you have any questions or concerns regarding these Terms,
you can reach us at:
</p>
<p className="mb-4">
Email:{" "}
Email:{' '}
<a
href="mailto:support@dokploy.com"
className="text-blue-500 hover:underline"
@@ -209,6 +228,5 @@ export default function TermsPage() {
</p>
</section>
</div>
);
)
}

File diff suppressed because one or more lines are too long

View File

@@ -1,13 +1,16 @@
import clsx from "clsx";
import clsx from 'clsx'
export function Container({
className,
...props
}: React.ComponentPropsWithoutRef<"div">) {
}: React.ComponentPropsWithoutRef<'div'>) {
return (
<div
className={clsx("mx-auto max-w-7xl px-4 sm:px-6 lg:px-8", className)}
className={clsx(
'mx-auto max-w-7xl px-4 sm:px-6 lg:px-8',
className,
)}
{...props}
/>
);
)
}

View File

@@ -3,79 +3,80 @@ import {
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Container } from "./Container";
} from '@/components/ui/accordion'
import { Container } from './Container'
const faqs = [
{
question: "What is Dokploy?",
answer: "Dokploy is a stable, easy-to-use deployment solution designed to simplify the application management process. Think of Dokploy as a free alternative self-hostable solution to platforms like Heroku, Vercel, and Netlify.",
question: 'What is Dokploy?',
answer: 'Dokploy is a stable, easy-to-use deployment solution designed to simplify the application management process. Think of Dokploy as a free alternative self-hostable solution to platforms like Heroku, Vercel, and Netlify.',
},
{
question: "How does Dokploy's Open Source plan work?",
answer: "You can host Dokploy UI on your own infrastructure and you will be responsible for the maintenance and updates.",
answer: '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 (e.g., Hetzner, Hostinger, AWS, etc.) VPS, and we manage the Dokploy UI infrastructure for you.",
question: 'Do I need to provide my own server for the managed plan?',
answer: 'Yes, in the managed plan, you provide your own server (e.g., 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: "The first server costs $4.50/month, if you buy more than one it will be $3.50/month per server.",
question: 'What happens if I need more than one server?',
answer: 'The first server costs $4.50/month, if you buy more than one it will be $3.50/month per server.',
},
{
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: '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: "What happens if I exceed my purchased server limit?",
question: 'What happens if I exceed my purchased server limit?',
answer: "The most recently added servers will be deactivated. You won't be able to create services on inactive servers until they are reactivated.",
},
{
question: "What kind of support do you offer?",
answer: "We offer community support for the open source version and priority support for paid plans (via Discord or Email at support@dokploy.com).",
question: 'What kind of support do you offer?',
answer: 'We offer community support for the open source version and priority support for paid plans (via Discord or Email at support@dokploy.com).',
},
{
question: "What's the catch on the Paid Plan?",
answer: "Nothing, once you link your server (VPS) to your account, you can deploy unlimited applications, databases, and users, and you get unlimited updates, deployments, backups, and more.",
answer: 'Nothing, once you link your server (VPS) to your account, you can deploy unlimited applications, databases, and users, and you get unlimited updates, deployments, backups, and more.',
},
{
question: "Why Choose Dokploy?",
answer: "Dokploy offers simplicity, flexibility, and speed in application deployment and management.",
question: 'Why Choose Dokploy?',
answer: 'Dokploy offers simplicity, flexibility, and speed in application deployment and management.',
},
{
question: "Is it open source?",
answer: "Yes, Dokploy is open source and free to use.",
question: 'Is it open source?',
answer: 'Yes, Dokploy is open source and free to use.',
},
{
question: "What types of languages can I deploy with Dokploy?",
answer: "Dokploy does not restrict programming languages. You are free to choose your preferred language and framework.",
question: 'What types of languages can I deploy with Dokploy?',
answer: 'Dokploy does not restrict programming languages. You are free to choose your preferred language and framework.',
},
{
question: "How do I request a feature or report a bug?",
answer: "To request a feature or report a bug, please create an issue on our GitHub repository or ask in our Discord channel.",
question: 'How do I request a feature or report a bug?',
answer: 'To request a feature or report a bug, please create an issue on our GitHub repository or ask in our Discord channel.',
},
{
question: "Do you track the usage of Dokploy?",
question: 'Do you track the usage of Dokploy?',
answer: "No, we don't track any usage data.",
},
{
question: "Are there any user forums or communities where I can interact with other users?",
answer: "Yes, we have active GitHub discussions and Discord where you can share ideas, ask for help, and connect with other users.",
question:
'Are there any user forums or communities where I can interact with other users?',
answer: 'Yes, we have active GitHub discussions and Discord where you can share ideas, ask for help, and connect with other users.',
},
{
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: '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: "What types of applications can I deploy with Dokploy?",
answer: "You can deploy any application that can be Dockerized, with no limits. Dokploy supports builds from Git repositories, Dockerfiles, Nixpacks, and Buildpacks like Heroku and Paketo.",
question: 'What types of applications can I deploy with Dokploy?',
answer: 'You can deploy any application that can be Dockerized, with no limits. Dokploy supports builds from Git repositories, Dockerfiles, Nixpacks, and Buildpacks like Heroku and Paketo.',
},
{
question: "How does Dokploy handle database management?",
answer: "Dokploy supports multiple database systems including Postgres, MySQL, MariaDB, MongoDB, and Redis, providing tools for easy deployment and management and backups directly from the dashboard.",
question: 'How does Dokploy handle database management?',
answer: 'Dokploy supports multiple database systems including Postgres, MySQL, MariaDB, MongoDB, and Redis, providing tools for easy deployment and management and backups directly from the dashboard.',
},
];
]
export function Faqs() {
return (
@@ -85,25 +86,30 @@ export function Faqs() {
className="relative overflow-hidden bg-black py-20 sm:py-32"
>
<Container className="relative flex flex-col gap-10">
<div className="mx-auto lg:mx-0 justify-center w-full">
<div className="mx-auto w-full justify-center lg:mx-0">
<h2
id="faq-title"
className="font-display text-3xl tracking-tight text-primary sm:text-4xl text-center"
className="text-center font-display text-3xl tracking-tight text-primary sm:text-4xl"
>
Frequently asked questions
</h2>
<p className="mt-4 text-lg tracking-tight text-muted-foreground text-center">
If you can't find what you're looking for, please submit an issue through our GitHub repository or ask questions on our Discord.
<p className="mt-4 text-center text-lg tracking-tight text-muted-foreground">
If you can't find what you're looking for, please submit
an issue through our GitHub repository or ask questions
on our Discord.
</p>
</div>
<Accordion
type="single"
collapsible
className="w-full max-w-3xl mx-auto"
className="mx-auto w-full max-w-3xl"
>
{faqs.map((faq, columnIndex) => (
<AccordionItem value={`${columnIndex}`} key={columnIndex}>
<AccordionItem
value={`${columnIndex}`}
key={columnIndex}
>
<AccordionTrigger className="text-left">
{faq.question}
</AccordionTrigger>
@@ -113,5 +119,5 @@ export function Faqs() {
</Accordion>
</Container>
</section>
);
)
}

View File

@@ -1,11 +1,11 @@
"use client";
'use client'
import Link from "next/link";
import type { SVGProps } from "react";
import { Container } from "./Container";
import { NavLink } from "./NavLink";
import { Logo } from "./shared/Logo";
import { buttonVariants } from "./ui/button";
import Link from 'next/link'
import type { SVGProps } from 'react'
import { Container } from './Container'
import { NavLink } from './NavLink'
import { Logo } from './shared/Logo'
import { buttonVariants } from './ui/button'
const I18nIcon = (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
<svg
@@ -23,7 +23,7 @@ const I18nIcon = (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
d="m478.33 433.6-90-218a22 22 0 0 0-40.67 0l-90 218a22 22 0 1 0 40.67 16.79L316.66 406h102.67l18.33 44.39A22 22 0 0 0 458 464a22 22 0 0 0 20.32-30.4zM334.83 362 368 281.65 401.17 362zm-66.99-19.08a22 22 0 0 0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73 39.65-53.68 62.11-114.75 71.27-143.49H330a22 22 0 0 0 0-44H214V70a22 22 0 0 0-44 0v20H54a22 22 0 0 0 0 44h197.25c-9.52 26.95-27.05 69.5-53.79 108.36-31.41-41.68-43.08-68.65-43.17-68.87a22 22 0 0 0-40.58 17c.58 1.38 14.55 34.23 52.86 83.93.92 1.19 1.83 2.35 2.74 3.51-39.24 44.35-77.74 71.86-93.85 80.74a22 22 0 1 0 21.07 38.63c2.16-1.18 48.6-26.89 101.63-85.59 22.52 24.08 38 35.44 38.93 36.1a22 22 0 0 0 30.75-4.9z"
/>
</svg>
);
)
export function Footer() {
return (
@@ -51,7 +51,7 @@ export function Footer() {
</nav>
</div>
<div className="flex flex-col items-center border-t border-slate-400/10 py-10 sm:flex-row-reverse sm:justify-between">
<div className="flex gap-x-6 items-center">
<div className="flex items-center gap-x-6">
<Link
href="https://x.com/getdokploy"
className="group"
@@ -87,5 +87,5 @@ export function Footer() {
</div>
</Container>
</footer>
);
)
}

View File

@@ -1,77 +1,79 @@
"use client";
'use client'
import Link from "next/link";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { cn } from '@/lib/utils'
type GithubStarsProps = {
className?: string;
repoUrl?: string;
label?: string;
count?: string;
};
className?: string
repoUrl?: string
label?: string
count?: string
}
// Function to format star count (e.g., 26400 -> "26.4k")
function formatStarCount(count: number): string {
if (count >= 1000000) {
return `${(count / 1000000).toFixed(1)}M`;
return `${(count / 1000000).toFixed(1)}M`
}
if (count >= 1000) {
return `${(count / 1000).toFixed(1)}k`;
return `${(count / 1000).toFixed(1)}k`
}
return count.toString();
return count.toString()
}
// Extract owner and repo from GitHub URL
function extractRepoInfo(url: string): { owner: string; repo: string } | null {
try {
const match = url.match(/github\.com\/([^\/]+)\/([^\/]+)/);
const match = url.match(/github\.com\/([^\/]+)\/([^\/]+)/)
if (match) {
return { owner: match[1], repo: match[2].replace(/\.git$/, "") };
return { owner: match[1], repo: match[2].replace(/\.git$/, '') }
}
} catch (error) {
console.error("Error extracting repo info:", error);
console.error('Error extracting repo info:', error)
}
return null;
return null
}
export function GithubStars({
className,
repoUrl = "https://github.com/dokploy/dokploy",
label = "GitHub Stars",
count: defaultCount = "26.4k",
repoUrl = 'https://github.com/dokploy/dokploy',
label = 'GitHub Stars',
count: defaultCount = '26.4k',
}: GithubStarsProps) {
const [starCount, setStarCount] = useState<string>(defaultCount);
const [isLoading, setIsLoading] = useState(true);
const [starCount, setStarCount] = useState<string>(defaultCount)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
const fetchStarCount = async () => {
const repoInfo = extractRepoInfo(repoUrl);
const repoInfo = extractRepoInfo(repoUrl)
if (!repoInfo) {
setIsLoading(false);
return;
setIsLoading(false)
return
}
try {
const response = await fetch(
`/api/github-stars?owner=${encodeURIComponent(repoInfo.owner)}&repo=${encodeURIComponent(repoInfo.repo)}`,
);
)
if (response.ok) {
const data = await response.json();
const formattedCount = formatStarCount(data.stargazers_count);
setStarCount(formattedCount);
const data = await response.json()
const formattedCount = formatStarCount(
data.stargazers_count,
)
setStarCount(formattedCount)
}
} catch (error) {
console.error("Error fetching GitHub stars:", error);
console.error('Error fetching GitHub stars:', error)
// Keep default count on error
} finally {
setIsLoading(false);
setIsLoading(false)
}
};
}
fetchStarCount();
}, [repoUrl]);
fetchStarCount()
}, [repoUrl])
return (
<Link
@@ -79,11 +81,11 @@ export function GithubStars({
target="_blank"
aria-label={`${label}: ${starCount}`}
className={cn(
"group relative inline-flex items-center gap-2 rounded-full px-3 py-1",
"shadow-[0_0_0_2px_#000_inset,0_2px_8px_rgba(0,0,0,0.35)]",
"bg-gradient-to-b from-yellow-300 via-yellow-400 to-yellow-500",
"text-black",
"transition-transform hover:scale-[1.02] active:scale-[0.99]",
'group relative inline-flex items-center gap-2 rounded-full px-3 py-1',
'shadow-[0_0_0_2px_#000_inset,0_2px_8px_rgba(0,0,0,0.35)]',
'bg-gradient-to-b from-yellow-300 via-yellow-400 to-yellow-500',
'text-black',
'transition-transform hover:scale-[1.02] active:scale-[0.99]',
className,
)}
>
@@ -96,9 +98,9 @@ export function GithubStars({
<svg
viewBox="0 0 24 24"
className={cn(
"absolute -top-1 -left-1 h-3 w-3 text-yellow-100",
"drop-shadow-[0_0_6px_rgba(255,255,200,0.9)]",
"animate-pulse [animation-duration:1.6s] [animation-delay:.2s]",
'absolute -left-1 -top-1 h-3 w-3 text-yellow-100',
'drop-shadow-[0_0_6px_rgba(255,255,200,0.9)]',
'animate-pulse [animation-delay:.2s] [animation-duration:1.6s]',
)}
fill="currentColor"
>
@@ -108,9 +110,9 @@ export function GithubStars({
<svg
viewBox="0 0 24 24"
className={cn(
"absolute -top-2 right-1 h-2.5 w-2.5 text-yellow-50",
"drop-shadow-[0_0_6px_rgba(255,255,220,0.95)]",
"animate-pulse [animation-duration:1.9s] [animation-delay:.7s]",
'absolute -top-2 right-1 h-2.5 w-2.5 text-yellow-50',
'drop-shadow-[0_0_6px_rgba(255,255,220,0.95)]',
'animate-pulse [animation-delay:.7s] [animation-duration:1.9s]',
)}
fill="currentColor"
>
@@ -120,9 +122,9 @@ export function GithubStars({
<svg
viewBox="0 0 24 24"
className={cn(
"absolute -bottom-1 -right-1 h-3.5 w-3.5 text-yellow-200",
"drop-shadow-[0_0_8px_rgba(255,255,180,0.85)]",
"animate-pulse [animation-duration:2.2s] [animation-delay:1.1s]",
'absolute -bottom-1 -right-1 h-3.5 w-3.5 text-yellow-200',
'drop-shadow-[0_0_8px_rgba(255,255,180,0.85)]',
'animate-pulse [animation-delay:1.1s] [animation-duration:2.2s]',
)}
fill="currentColor"
>
@@ -137,10 +139,10 @@ export function GithubStars({
>
<span
className={cn(
"absolute -inset-x-10 -top-6 h-10 rotate-12",
"bg-white/40 blur-md",
"opacity-0 transition-opacity duration-500",
"group-hover:opacity-40",
'absolute -inset-x-10 -top-6 h-10 rotate-12',
'bg-white/40 blur-md',
'opacity-0 transition-opacity duration-500',
'group-hover:opacity-40',
)}
/>
</span>
@@ -148,9 +150,9 @@ export function GithubStars({
{/* GitHub mark */}
<span
className={cn(
"flex h-6 w-6 items-center justify-center rounded-full",
"bg-black text-white",
"shadow-[inset_0_0_0_1px_rgba(255,255,255,0.15)]",
'flex h-6 w-6 items-center justify-center rounded-full',
'bg-black text-white',
'shadow-[inset_0_0_0_1px_rgba(255,255,255,0.15)]',
)}
>
<svg
@@ -167,7 +169,7 @@ export function GithubStars({
<span className="flex items-baseline gap-1 pr-0.5">
<span className="text-xs font-semibold">Stars</span>
<span className="text-sm font-extrabold tracking-tight">
{isLoading ? "..." : starCount}
{isLoading ? '...' : starCount}
</span>
</span>
@@ -175,12 +177,12 @@ export function GithubStars({
<span
aria-hidden
className={cn(
"pointer-events-none absolute inset-0 rounded-full",
"ring-1 ring-black/10 group-hover:ring-black/20",
'pointer-events-none absolute inset-0 rounded-full',
'ring-1 ring-black/10 group-hover:ring-black/20',
)}
/>
</Link>
);
)
}
export default GithubStars;
export default GithubStars

View File

@@ -1,35 +1,35 @@
"use client";
'use client'
import Link from "next/link";
import { cn } from "@/lib/utils";
import { Popover, Transition } from "@headlessui/react";
import { ChevronRight, HeartIcon } from "lucide-react";
import { Fragment, type JSX, type SVGProps } from "react";
import { Container } from "./Container";
import { NavLink } from "./NavLink";
import { trackGAEvent } from "./analitycs";
import { Logo } from "./shared/Logo";
import AnimatedGradientText from "./ui/animated-gradient-text";
import { Button, buttonVariants } from "./ui/button";
import GithubStars from "./GithubStars";
import Link from 'next/link'
import { cn } from '@/lib/utils'
import { Popover, Transition } from '@headlessui/react'
import { ChevronRight, HeartIcon } from 'lucide-react'
import { Fragment, type JSX, type SVGProps } from 'react'
import { Container } from './Container'
import { NavLink } from './NavLink'
import { trackGAEvent } from './analitycs'
import { Logo } from './shared/Logo'
import AnimatedGradientText from './ui/animated-gradient-text'
import { Button, buttonVariants } from './ui/button'
import GithubStars from './GithubStars'
function MobileNavLink({
href,
children,
target,
}: {
href: string;
children: React.ReactNode;
target?: string;
href: string
children: React.ReactNode
target?: string
}) {
return (
<Popover.Button
onClick={() => {
trackGAEvent({
action: "Nav Link Clicked",
category: "Navigation",
action: 'Nav Link Clicked',
category: 'Navigation',
label: href,
});
})
}}
as={Link}
href={href}
@@ -38,7 +38,7 @@ function MobileNavLink({
>
{children}
</Popover.Button>
);
)
}
function MobileNavIcon({ open }: { open: boolean }) {
@@ -52,17 +52,20 @@ function MobileNavIcon({ open }: { open: boolean }) {
>
<path
d="M0 1H14M0 7H14M0 13H14"
className={cn("origin-center transition", open && "scale-90 opacity-0")}
className={cn(
'origin-center transition',
open && 'scale-90 opacity-0',
)}
/>
<path
d="M2 2L12 12M12 2L2 12"
className={cn(
"origin-center transition",
!open && "scale-90 opacity-0",
'origin-center transition',
!open && 'scale-90 opacity-0',
)}
/>
</svg>
);
)
}
const I18nIcon = (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
@@ -81,7 +84,7 @@ const I18nIcon = (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
d="m478.33 433.6-90-218a22 22 0 0 0-40.67 0l-90 218a22 22 0 1 0 40.67 16.79L316.66 406h102.67l18.33 44.39A22 22 0 0 0 458 464a22 22 0 0 0 20.32-30.4zM334.83 362 368 281.65 401.17 362zm-66.99-19.08a22 22 0 0 0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73 39.65-53.68 62.11-114.75 71.27-143.49H330a22 22 0 0 0 0-44H214V70a22 22 0 0 0-44 0v20H54a22 22 0 0 0 0 44h197.25c-9.52 26.95-27.05 69.5-53.79 108.36-31.41-41.68-43.08-68.65-43.17-68.87a22 22 0 0 0-40.58 17c.58 1.38 14.55 34.23 52.86 83.93.92 1.19 1.83 2.35 2.74 3.51-39.24 44.35-77.74 71.86-93.85 80.74a22 22 0 1 0 21.07 38.63c2.16-1.18 48.6-26.89 101.63-85.59 22.52 24.08 38 35.44 38.93 36.1a22 22 0 0 0 30.75-4.9z"
/>
</svg>
);
)
function MobileNavigation() {
return (
@@ -138,7 +141,7 @@ function MobileNavigation() {
aria-label="Sign In Dokploy Cloud"
target="_blank"
>
<div className="group flex-row relative mx-auto flex max-w-fit items-center justify-center rounded-2xl text-sm font-medium w-full">
<div className="group relative mx-auto flex w-full max-w-fit flex-row items-center justify-center rounded-2xl text-sm font-medium">
<span>Sign In</span>
<ChevronRight className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
</div>
@@ -149,12 +152,12 @@ function MobileNavigation() {
</Transition.Child>
</Transition.Root>
</Popover>
);
)
}
export function Header() {
return (
<header className="sticky top-0 z-50 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b border-border/40 py-5">
<header className="sticky top-0 z-50 border-b border-border/40 bg-background/95 py-5 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<Container>
<nav className="relative z-50 flex justify-between">
<div className="flex items-center md:gap-x-12">
@@ -183,7 +186,7 @@ export function Header() {
strokeWidth="0"
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 fill-muted-foreground group-hover:fill-muted-foreground/70 hover:fill-muted-foreground/80"
className="h-5 w-5 fill-muted-foreground hover:fill-muted-foreground/80 group-hover:fill-muted-foreground/70"
>
<path d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z" />
</svg>
@@ -198,10 +201,10 @@ export function Header() {
href="/contact"
onClick={() => {
trackGAEvent({
action: "Contact Button Clicked",
category: "Contact",
label: "Header",
});
action: 'Contact Button Clicked',
category: 'Contact',
label: 'Header',
})
}}
>
Contact
@@ -228,7 +231,7 @@ export function Header() {
aria-label="Sign In Dokploy Cloud"
target="_blank"
>
<div className="group flex-row relative mx-auto flex max-w-fit items-center justify-center rounded-2xl text-sm font-medium w-full">
<div className="group relative mx-auto flex w-full max-w-fit flex-row items-center justify-center rounded-2xl text-sm font-medium">
<span>Sign In</span>
<ChevronRight className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
</div>
@@ -241,5 +244,5 @@ export function Header() {
</nav>
</Container>
</header>
);
)
}

View File

@@ -1,13 +1,13 @@
"use client";
import { cn } from "@/lib/utils";
import { motion } from "framer-motion";
import { Check, ChevronRight, Copy } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import AnimatedGradientText from "./ui/animated-gradient-text";
import AnimatedGridPattern from "./ui/animated-grid-pattern";
import { Button } from "./ui/button";
import HeroVideoDialog from "./ui/hero-video-dialog";
'use client'
import { cn } from '@/lib/utils'
import { motion } from 'framer-motion'
import { Check, ChevronRight, Copy } from 'lucide-react'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import AnimatedGradientText from './ui/animated-gradient-text'
import AnimatedGridPattern from './ui/animated-grid-pattern'
import { Button } from './ui/button'
import HeroVideoDialog from './ui/hero-video-dialog'
// const ProductHunt = () => {
// return (
@@ -42,14 +42,14 @@ import HeroVideoDialog from "./ui/hero-video-dialog";
// };
export function Hero() {
const [isCopied, setIsCopied] = useState(false);
const [isCopied, setIsCopied] = useState(false)
useEffect(() => {
const timer = setTimeout(() => {
setIsCopied(false);
}, 2000);
return () => clearTimeout(timer);
}, [isCopied]);
setIsCopied(false)
}, 2000)
return () => clearTimeout(timer)
}, [isCopied])
return (
<div className="h-[1100px] bg-black pt-20 sm:h-[1100px] lg:pt-32">
<div className=" bottom-0 flex w-full items-center justify-center overflow-hidden rounded-lg bg-background md:shadow-xl">
@@ -77,34 +77,38 @@ export function Hero() {
</div>
</motion.a> */}
<motion.h1
className="mx-auto max-w-4xl font-display text-5xl font-medium tracking-tight text-muted-foreground sm:text-7xl"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
Simplify{" "}
<span className="relative whitespace-nowrap text-primary">
<svg
aria-hidden="true"
viewBox="0 0 418 42"
className="absolute left-0 top-2/3 h-[0.58em] w-full fill-primary"
preserveAspectRatio="none"
<motion.h1
className="mx-auto max-w-4xl font-display text-5xl font-medium tracking-tight text-muted-foreground sm:text-7xl"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<path d="M203.371.916c-26.013-2.078-76.686 1.963-124.73 9.946L67.3 12.749C35.421 18.062 18.2 21.766 6.004 25.934 1.244 27.561.828 27.778.874 28.61c.07 1.214.828 1.121 9.595-1.176 9.072-2.377 17.15-3.92 39.246-7.496C123.565 7.986 157.869 4.492 195.942 5.046c7.461.108 19.25 1.696 19.17 2.582-.107 1.183-7.874 4.31-25.75 10.366-21.992 7.45-35.43 12.534-36.701 13.884-2.173 2.308-.202 4.407 4.442 4.734 2.654.187 3.263.157 15.593-.78 35.401-2.686 57.944-3.488 88.365-3.143 46.327.526 75.721 2.23 130.788 7.584 19.787 1.924 20.814 1.98 24.557 1.332l.066-.011c1.201-.203 1.53-1.825.399-2.335-2.911-1.31-4.893-1.604-22.048-3.261-57.509-5.556-87.871-7.36-132.059-7.842-23.239-.254-33.617-.116-50.627.674-11.629.54-42.371 2.494-46.696 2.967-2.359.259 8.133-3.625 26.504-9.81 23.239-7.825 27.934-10.149 28.304-14.005.417-4.348-3.529-6-16.878-7.066Z" />
</svg>
<span className="relative">Application and Database</span>
</span>{" "}
Deployments
</motion.h1>
<motion.p
className="mx-auto mt-6 max-w-2xl text-lg tracking-tight text-muted-foreground"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.2 }}
>
Manage containerized deployments across multiple servers with ease thanks to our all-in-one platform for developers.
</motion.p>
Simplify{' '}
<span className="relative whitespace-nowrap text-primary">
<svg
aria-hidden="true"
viewBox="0 0 418 42"
className="absolute left-0 top-2/3 h-[0.58em] w-full fill-primary"
preserveAspectRatio="none"
>
<path d="M203.371.916c-26.013-2.078-76.686 1.963-124.73 9.946L67.3 12.749C35.421 18.062 18.2 21.766 6.004 25.934 1.244 27.561.828 27.778.874 28.61c.07 1.214.828 1.121 9.595-1.176 9.072-2.377 17.15-3.92 39.246-7.496C123.565 7.986 157.869 4.492 195.942 5.046c7.461.108 19.25 1.696 19.17 2.582-.107 1.183-7.874 4.31-25.75 10.366-21.992 7.45-35.43 12.534-36.701 13.884-2.173 2.308-.202 4.407 4.442 4.734 2.654.187 3.263.157 15.593-.78 35.401-2.686 57.944-3.488 88.365-3.143 46.327.526 75.721 2.23 130.788 7.584 19.787 1.924 20.814 1.98 24.557 1.332l.066-.011c1.201-.203 1.53-1.825.399-2.335-2.911-1.31-4.893-1.604-22.048-3.261-57.509-5.556-87.871-7.36-132.059-7.842-23.239-.254-33.617-.116-50.627.674-11.629.54-42.371 2.494-46.696 2.967-2.359.259 8.133-3.625 26.504-9.81 23.239-7.825 27.934-10.149 28.304-14.005.417-4.348-3.529-6-16.878-7.066Z" />
</svg>
<span className="relative">
Application and Database
</span>
</span>{' '}
Deployments
</motion.h1>
<motion.p
className="mx-auto mt-6 max-w-2xl text-lg tracking-tight text-muted-foreground"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.2 }}
>
Manage containerized deployments across multiple
servers with ease thanks to our all-in-one platform
for developers.
</motion.p>
<motion.div
className="flex flex-col items-center justify-center space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0"
initial={{ opacity: 0, y: 20 }}
@@ -114,16 +118,21 @@ export function Hero() {
<div className="flex flex-col gap-6">
<div className="mt-6 flex flex-wrap items-center justify-center gap-6 md:flex-nowrap">
<code className="flex flex-row items-center gap-4 rounded-xl border p-3 font-sans">
curl -sSL https://dokploy.com/install.sh | sh
curl -sSL https://dokploy.com/install.sh
| sh
<button
type="button"
onClick={() =>
navigator.clipboard
.writeText(
"curl -sSL https://dokploy.com/install.sh | sh",
'curl -sSL https://dokploy.com/install.sh | sh',
)
.then(() =>
setIsCopied(true),
)
.catch(() =>
setIsCopied(false),
)
.then(() => setIsCopied(true))
.catch(() => setIsCopied(false))
}
>
{isCopied ? (
@@ -135,14 +144,20 @@ export function Hero() {
</code>
</div>
<div className="mx-auto flex w-full max-w-sm flex-wrap items-center justify-center gap-3 md:flex-nowrap">
<Button className="w-full rounded-full" asChild>
<Button
className="w-full rounded-full"
asChild
>
<Link
href="https://github.com/dokploy/dokploy"
aria-label="Dokploy on GitHub"
target="_blank"
className="flex flex-row items-center gap-2"
>
<svg aria-hidden="true" className="h-6 w-6 fill-black">
<svg
aria-hidden="true"
className="h-6 w-6 fill-black"
>
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2Z" />
</svg>
GitHub
@@ -198,11 +213,11 @@ export function Hero() {
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",
'[mask-image:radial-gradient(800px_circle_at_center,white,transparent)]',
'absolute inset-x-0 inset-y-[-30%] h-[200%] skew-y-12',
)}
/>
</div>
</div>
);
)
}

View File

@@ -1,16 +1,16 @@
"use client";
'use client'
import Link from "next/link";
import { trackGAEvent } from "./analitycs";
import Link from 'next/link'
import { trackGAEvent } from './analitycs'
export function NavLink({
href,
children,
target,
}: {
href: string;
children: React.ReactNode;
target?: string;
href: string
children: React.ReactNode
target?: string
}) {
return (
<div>
@@ -18,16 +18,16 @@ export function NavLink({
href={href}
onClick={() =>
trackGAEvent({
action: "Nav Link Clicked",
category: "Navigation",
action: 'Nav Link Clicked',
category: 'Navigation',
label: href,
})
}
target={target}
className="inline-block self-center rounded-lg px-2.5 py-1.5 text-sm text-popover-foreground font-medium transition-colors hover:text-primary hover:bg-secondary"
className="inline-block self-center rounded-lg px-2.5 py-1.5 text-sm font-medium text-popover-foreground transition-colors hover:bg-secondary hover:text-primary"
>
{children}
</Link>
</div>
);
)
}

View File

@@ -1,37 +1,40 @@
"use client";
'use client'
import { cn } from "@/lib/utils";
import { Tab } from "@headlessui/react";
import { motion } from "framer-motion";
import { Layers, Terminal, Users } from "lucide-react";
import { Container } from "./Container";
import { cn } from '@/lib/utils'
import { Tab } from '@headlessui/react'
import { motion } from 'framer-motion'
import { Layers, Terminal, Users } from 'lucide-react'
import { Container } from './Container'
interface Feature {
name: string;
summary: string;
description: string;
image: string;
icon: React.ComponentType;
name: string
summary: string
description: string
image: string
icon: React.ComponentType
}
const features: Array<Feature> = [
{
name: "Open Source Templates",
summary: "One click to deploy open source templates.",
description: "Deploy open source templates with one click, powered by Docker Compose, (Plausible, Calcom, Pocketbase, etc.)",
image: "/secondary/templates.png",
name: 'Open Source Templates',
summary: 'One click to deploy open source templates.',
description:
'Deploy open source templates with one click, powered by Docker Compose, (Plausible, Calcom, Pocketbase, etc.)',
image: '/secondary/templates.png',
icon: function ReportingIcon() {
return (
<>
<Layers className="size-5 text-primary" />
</>
);
)
},
},
{
name: "Real-Time Traefik Configuration",
summary: "Modify Traefik settings on-the-fly via a graphical interface or API.",
description: "Users can adjust Traefik's configuration, including middleware, forwarding rules, and SSL certificates through an intuitive interface or API. This feature enables seamless traffic routing and security adjustments without the need to restart services",
image: "/secondary/traefik.png",
name: 'Real-Time Traefik Configuration',
summary:
'Modify Traefik settings on-the-fly via a graphical interface or API.',
description:
"Users can adjust Traefik's configuration, including middleware, forwarding rules, and SSL certificates through an intuitive interface or API. This feature enables seamless traffic routing and security adjustments without the need to restart services",
image: '/secondary/traefik.png',
icon: function ReportingIcon() {
return (
<>
@@ -81,7 +84,12 @@ const features: Array<Feature> = [
d="M299.847 285.567c10.027 58.288 105.304 42.877 91.619-15.91-12.271-52.716-94.951-38.124-91.619 15.91m-113.855 9.427c12.996 50.745 94.24 37.753 91.178-13.149-3.669-60.964-103.603-49.2-91.178 13.149m132.351 58.517c.044 7.79 1.843 15.403.289 24.148-1.935 3.656-5.729 4.043-9.001 5.52-4.524-.71-8.328-3.68-10.143-7.912-1.161-9.202.433-18.111.726-27.316l18.129 5.56z"
fill="#fff"
/>
<ellipse cx="208.4" cy="286.718" rx="13.719" ry="14.86" />
<ellipse
cx="208.4"
cy="286.718"
rx="13.719"
ry="14.86"
/>
<ellipse
cx="214.64"
cy="290.071"
@@ -89,9 +97,19 @@ const features: Array<Feature> = [
ry="3.777"
fill="#fff"
/>
<ellipse cx="323.348" cy="283.017" rx="13.491" ry="14.86" />
<ellipse
cx="323.348"
cy="283.017"
rx="13.491"
ry="14.86"
/>
<g fill="#fff">
<ellipse cx="329.485" cy="286.371" rx="3.181" ry="3.777" />
<ellipse
cx="329.485"
cy="286.371"
rx="3.181"
ry="3.777"
/>
<path d="M279.137 354.685c-5.986 14.507 3.338 43.515 19.579 22.119-1.161-9.202.433-18.111.726-27.316l-20.305 5.197z" />
</g>
<path
@@ -210,59 +228,63 @@ const features: Array<Feature> = [
</g>
</svg>
</>
);
)
},
},
{
name: "User Permission Management",
summary: "Detailed control over user permissions for accessing and managing projects and services.",
description: "Allows administrators to define specific roles and permissions for each user, including the ability to create, modify, or delete applications and databases. This feature ensures secure and efficient management of large and diverse teams.",
image: "/secondary/users.png",
name: 'User Permission Management',
summary:
'Detailed control over user permissions for accessing and managing projects and services.',
description:
'Allows administrators to define specific roles and permissions for each user, including the ability to create, modify, or delete applications and databases. This feature ensures secure and efficient management of large and diverse teams.',
image: '/secondary/users.png',
icon: function InventoryIcon() {
return (
<>
<Users className="size-5 text-primary" />
</>
);
)
},
},
{
name: "Terminal Access",
summary: "Direct access to each container's and server terminal for advanced management.",
description: "Provides an interface to access the command line of any active container, allowing developers to execute commands, manage services, and troubleshoot directly from the dashboard",
image: "/secondary/terminal.png",
name: 'Terminal Access',
summary:
"Direct access to each container's and server terminal for advanced management.",
description:
'Provides an interface to access the command line of any active container, allowing developers to execute commands, manage services, and troubleshoot directly from the dashboard',
image: '/secondary/terminal.png',
icon: function ContactsIcon() {
return (
<>
<Terminal className="size-5 text-primary" />
</>
);
)
},
},
];
]
function Feature({
feature,
isActive,
className,
...props
}: React.ComponentPropsWithoutRef<"div"> & {
feature: Feature;
isActive: boolean;
}: React.ComponentPropsWithoutRef<'div'> & {
feature: Feature
isActive: boolean
}) {
return (
<div
className={cn(
className,
!isActive ? "opacity-75 hover:opacity-100 " : "rounded-xl",
" relative p-4",
!isActive ? 'opacity-75 hover:opacity-100 ' : 'rounded-xl',
' relative p-4',
)}
{...props}
>
<div
className={cn(
"flex size-9 items-center justify-center rounded-lg",
isActive ? "bg-border" : "bg-muted",
'flex size-9 items-center justify-center rounded-lg',
isActive ? 'bg-border' : 'bg-muted',
)}
>
<feature.icon />
@@ -272,7 +294,7 @@ function Feature({
layoutId="bubble"
className="absolute inset-0 z-10 rounded-xl bg-white/5 mix-blend-difference"
transition={{
type: "spring",
type: 'spring',
bounce: 0.2,
duration: 0.6,
}}
@@ -280,8 +302,8 @@ function Feature({
)}
<h3
className={cn(
"mt-6 text-sm font-medium",
isActive ? "text-primary" : "text-primary/85",
'mt-6 text-sm font-medium',
isActive ? 'text-primary' : 'text-primary/85',
)}
>
{feature.name}
@@ -293,7 +315,7 @@ function Feature({
{feature.description}
</p>
</div>
);
)
}
function FeaturesMobile() {
@@ -301,7 +323,11 @@ function FeaturesMobile() {
<div className="-mx-4 mt-20 flex flex-col gap-y-10 overflow-hidden px-4 sm:-mx-6 sm:px-6 lg:hidden">
{features.map((feature) => (
<div key={feature.summary}>
<Feature feature={feature} className="mx-auto max-w-2xl" isActive />
<Feature
feature={feature}
className="mx-auto max-w-2xl"
isActive
/>
<div className="relative mt-10 pb-10">
<div className="absolute -inset-x-4 bottom-0 top-8 bg-muted sm:-inset-x-6" />
<div className="relative mx-auto w-[52.75rem] overflow-hidden rounded-xl bg-white shadow-lg shadow-slate-900/5 ring-1 ring-slate-500/10">
@@ -316,7 +342,7 @@ function FeaturesMobile() {
</div>
))}
</div>
);
)
}
function FeaturesDesktop() {
@@ -349,8 +375,9 @@ function FeaturesDesktop() {
static
key={feature.summary}
className={cn(
"px-5 transition duration-500 ease-in-out ui-not-focus-visible:outline-none",
featureIndex !== selectedIndex && "opacity-60",
'px-5 transition duration-500 ease-in-out ui-not-focus-visible:outline-none',
featureIndex !== selectedIndex &&
'opacity-60',
)}
style={{
transform: `translateX(-${selectedIndex * 100}%)`,
@@ -373,7 +400,7 @@ function FeaturesDesktop() {
</>
)}
</Tab.Group>
);
)
}
export function SecondaryFeatures() {
@@ -389,12 +416,15 @@ export function SecondaryFeatures() {
Advanced Management Tools
</h2>
<p className="mt-4 text-lg tracking-tight text-muted-foreground">
Elevate your infrastructure with tools that offer precise control, detailed monitoring, and enhanced security, ensuring seamless management and robust performance.
Elevate your infrastructure with tools that offer
precise control, detailed monitoring, and enhanced
security, ensuring seamless management and robust
performance.
</p>
</div>
<FeaturesMobile />
<FeaturesDesktop />
</Container>
</section>
);
)
}

View File

@@ -1,12 +1,16 @@
import Link from "next/link";
import Link from 'next/link'
export function SlimLayout() {
return (
<>
<main className="flex flex-auto items-center justify-center text-center">
<div>
<h1 className="mb-4 text-6xl font-semibold text-primary">404</h1>
<p className="mb-4 text-lg text-muted-foreground">Not found.</p>
<h1 className="mb-4 text-6xl font-semibold text-primary">
404
</h1>
<p className="mb-4 text-lg text-muted-foreground">
Not found.
</p>
<p className="mt-4 text-muted-foreground">
Go back to home
<Link href="/" className="text-primary">
@@ -17,5 +21,5 @@ export function SlimLayout() {
</div>
</main>
</>
);
)
}

View File

@@ -1,6 +1,6 @@
"use client";
import { cn } from "@/lib/utils";
import { Marquee } from "./ui/marquee";
'use client'
import { cn } from '@/lib/utils'
import { Marquee } from './ui/marquee'
// const testimonials = [
// [
@@ -75,75 +75,75 @@ import { Marquee } from "./ui/marquee";
const reviews = [
{
name: "Duras",
username: "@duras",
body: "This app convinced me to try something beyond pure Docker Compose. Its a pleasure to contribute to such an awesome project!",
img: "https://avatar.vercel.sh/duras",
name: 'Duras',
username: '@duras',
body: 'This app convinced me to try something beyond pure Docker Compose. Its a pleasure to contribute to such an awesome project!',
img: 'https://avatar.vercel.sh/duras',
},
{
name: "apis",
username: "@apis",
body: "I replaced my previous setup with Dokploy today. Its stable, easy to use, and offers excellent support!",
img: "https://avatar.vercel.sh/apis",
name: 'apis',
username: '@apis',
body: 'I replaced my previous setup with Dokploy today. Its stable, easy to use, and offers excellent support!',
img: 'https://avatar.vercel.sh/apis',
},
{
name: "yayza_",
username: "@yayza_",
body: "Migrated all my services to Dokploy—it worked seamlessly! The level of configuration is perfect for all kinds of projects.",
img: "https://avatar.vercel.sh/yayza",
name: 'yayza_',
username: '@yayza_',
body: 'Migrated all my services to Dokploy—it worked seamlessly! The level of configuration is perfect for all kinds of projects.',
img: 'https://avatar.vercel.sh/yayza',
},
{
name: "Vaurion",
username: "@vaurion",
body: "Dokploy makes my deployments incredibly easy. I just test locally, push a preview to GitHub, and Dokploy takes care of the rest.",
img: "https://avatar.vercel.sh/vaurion",
name: 'Vaurion',
username: '@vaurion',
body: 'Dokploy makes my deployments incredibly easy. I just test locally, push a preview to GitHub, and Dokploy takes care of the rest.',
img: 'https://avatar.vercel.sh/vaurion',
},
{
name: "vinum?",
username: "@vinum",
name: 'vinum?',
username: '@vinum',
body: "Dokploy is everything I wanted in a PaaS. The functionality is impressive, and it's completely free!",
img: "https://avatar.vercel.sh/vinum",
img: 'https://avatar.vercel.sh/vinum',
},
{
name: "vadzim",
username: "@vadzim",
body: "Dokploy is fantastic! I rarely encounter any deployment issues, and the community support is top-notch.",
img: "https://avatar.vercel.sh/vadzim",
name: 'vadzim',
username: '@vadzim',
body: 'Dokploy is fantastic! I rarely encounter any deployment issues, and the community support is top-notch.',
img: 'https://avatar.vercel.sh/vadzim',
},
{
name: "Slurpy Beckerman",
username: "@slurpy",
body: "This is exactly what I want in a deployment system. Ive restructured my dev process around Dokploy!",
img: "https://avatar.vercel.sh/slurpy",
name: 'Slurpy Beckerman',
username: '@slurpy',
body: 'This is exactly what I want in a deployment system. Ive restructured my dev process around Dokploy!',
img: 'https://avatar.vercel.sh/slurpy',
},
{
name: "lua",
username: "@lua",
body: "Dokploy is genuinely so nice to use. The hard work behind it really shows.",
img: "https://avatar.vercel.sh/lua",
name: 'lua',
username: '@lua',
body: 'Dokploy is genuinely so nice to use. The hard work behind it really shows.',
img: 'https://avatar.vercel.sh/lua',
},
{
name: "johnnygri",
username: "@johnnygri",
body: "Dokploy is a complete joy to use. Im running a mix of critical and low-priority services seamlessly across servers.",
img: "https://avatar.vercel.sh/johnnygri",
name: 'johnnygri',
username: '@johnnygri',
body: 'Dokploy is a complete joy to use. Im running a mix of critical and low-priority services seamlessly across servers.',
img: 'https://avatar.vercel.sh/johnnygri',
},
{
name: "HiJoe",
username: "@hijoe",
body: "Setting up Dokploy was great—simple, intuitive, and reliable. Perfect for small to medium-sized businesses.",
img: "https://avatar.vercel.sh/hijoe",
name: 'HiJoe',
username: '@hijoe',
body: 'Setting up Dokploy was great—simple, intuitive, and reliable. Perfect for small to medium-sized businesses.',
img: 'https://avatar.vercel.sh/hijoe',
},
{
name: "johannes0910",
username: "@johannes0910",
body: "Dokploy has been a game-changer for my side projects. Solid UI, straightforward Docker abstraction, and great design.",
img: "https://avatar.vercel.sh/johannes0910",
name: 'johannes0910',
username: '@johannes0910',
body: 'Dokploy has been a game-changer for my side projects. Solid UI, straightforward Docker abstraction, and great design.',
img: 'https://avatar.vercel.sh/johannes0910',
},
];
]
const firstRow = reviews.slice(0, reviews.length / 2);
const secondRow = reviews.slice(reviews.length / 2);
const firstRow = reviews.slice(0, reviews.length / 2)
const secondRow = reviews.slice(reviews.length / 2)
const ReviewCard = ({
img,
@@ -151,34 +151,42 @@ const ReviewCard = ({
username,
body,
}: {
img: string;
name: string;
username: string;
body: string;
img: string
name: string
username: string
body: string
}) => {
return (
<figure
className={cn(
"relative w-64 cursor-pointer overflow-hidden rounded-xl border p-4",
'relative w-64 cursor-pointer overflow-hidden rounded-xl border p-4',
// light styles
// "border-gray-950/[.1] bg-gray-950/[.01] hover:bg-gray-950/[.05]",
// dark styles
"hover:bg-gray-50/[.15]",
'hover:bg-gray-50/[.15]',
)}
>
<div className="flex flex-row items-center gap-2">
<img className="rounded-full" width="32" height="32" alt="" src={img} />
<img
className="rounded-full"
width="32"
height="32"
alt=""
src={img}
/>
<div className="flex flex-col">
<figcaption className="text-sm font-medium text-white">
{name}
</figcaption>
<p className="text-xs font-medium text-white/40">{username}</p>
<p className="text-xs font-medium text-white/40">
{username}
</p>
</div>
</div>
<blockquote className="mt-2 text-sm">{body}</blockquote>
</figure>
);
};
)
}
export function Testimonials() {
return (
@@ -187,13 +195,14 @@ export function Testimonials() {
aria-label="What our customers are saying"
className=" py-20 sm:py-32"
>
<div className="mx-auto max-w-2xl md:text-center px-4">
<h2 className="font-display text-3xl tracking-tight sm:text-4xl text-center">
<div className="mx-auto max-w-2xl px-4 md:text-center">
<h2 className="text-center font-display text-3xl tracking-tight sm:text-4xl">
Why Developers Love Dokploy
</h2>
<p className="mt-4 text-lg tracking-tight text-muted-foreground text-center">
Think were bragging? Hear from the devs who once doubted toountil
Dokploy made their lives (and deployments) surprisingly easier.
<p className="mt-4 text-center text-lg tracking-tight text-muted-foreground">
Think were bragging? Hear from the devs who once doubted
toountil Dokploy made their lives (and deployments)
surprisingly easier.
</p>
</div>
@@ -212,5 +221,5 @@ export function Testimonials() {
<div className="pointer-events-none absolute inset-y-0 right-0 w-1/3 bg-gradient-to-l from-background" />
</div>
</section>
);
)
}

View File

@@ -1,17 +1,17 @@
"use client";
'use client'
import { useEffect } from "react";
import initializeGA from ".";
import { useEffect } from 'react'
import initializeGA from '.'
export default function GoogleAnalytics() {
useEffect(() => {
// @ts-ignore
if (!window.GA_INITIALIZED) {
initializeGA();
initializeGA()
// @ts-ignore
window.GA_INITIALIZED = true;
window.GA_INITIALIZED = true
}
}, []);
}, [])
return null;
return null
}

View File

@@ -1,30 +1,30 @@
"use client";
'use client'
import ReactGA from "react-ga4";
import ReactGA from 'react-ga4'
const initializeGA = () => {
// Replace with your Measurement ID
// It ideally comes from an environment variable
ReactGA.initialize("G-0RTZ5EPB26");
ReactGA.initialize('G-0RTZ5EPB26')
// Don't forget to remove the console.log() statements
// when you are done
};
}
interface Props {
category: string;
action: string;
label: string;
category: string
action: string
label: string
}
const trackGAEvent = ({ category, action, label }: Props) => {
console.log("GA event:", category, ":", action, ":", label);
console.log('GA event:', category, ':', action, ':', label)
// Send GA4 Event
ReactGA.event({
category: category,
action: action,
label: label,
});
};
})
}
export default initializeGA;
export { initializeGA, trackGAEvent };
export default initializeGA
export { initializeGA, trackGAEvent }

View File

@@ -1,17 +1,17 @@
import type { Post } from "@/lib/ghost";
import Image from "next/image";
import Link from "next/link";
import type { Post } from '@/lib/ghost'
import Image from 'next/image'
import Link from 'next/link'
interface BlogCardProps {
post: Post;
post: Post
}
export function BlogCard({ post }: BlogCardProps) {
const formattedDate = new Date(post.published_at).toLocaleDateString("en", {
year: "numeric",
month: "long",
day: "numeric",
});
const formattedDate = new Date(post.published_at).toLocaleDateString('en', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
return (
<div className="flex flex-col overflow-hidden rounded-lg shadow-lg transition-all hover:shadow-xl">
@@ -40,7 +40,9 @@ export function BlogCard({ post }: BlogCardProps) {
<h3 className="text-xl font-semibold text-gray-900">
{post.title}
</h3>
<p className="mt-3 text-base text-gray-500">{post.excerpt}</p>
<p className="mt-3 text-base text-gray-500">
{post.excerpt}
</p>
</Link>
</div>
<div className="mt-6 flex items-center">
@@ -56,10 +58,12 @@ export function BlogCard({ post }: BlogCardProps) {
)}
<div className="ml-3">
<p className="text-sm font-medium text-gray-900">
{post.primary_author?.name || "Anonymous"}
{post.primary_author?.name || 'Anonymous'}
</p>
<div className="flex space-x-1 text-sm text-gray-500">
<time dateTime={post.published_at}>{formattedDate}</time>
<time dateTime={post.published_at}>
{formattedDate}
</time>
<span aria-hidden="true">&middot;</span>
<span>{post.reading_time} min read</span>
</div>
@@ -67,5 +71,5 @@ export function BlogCard({ post }: BlogCardProps) {
</div>
</div>
</div>
);
)
}

View File

@@ -1,161 +1,170 @@
import { cn } from "@/lib/utils";
import { IconBrandYoutubeFilled } from "@tabler/icons-react";
import { motion } from "framer-motion";
import Image from "next/image";
import Link from "next/link";
import type React from "react";
import { cn } from '@/lib/utils'
import { IconBrandYoutubeFilled } from '@tabler/icons-react'
import { motion } from 'framer-motion'
import Image from 'next/image'
import Link from 'next/link'
import type React from 'react'
export function FeaturesSectionDemo() {
const features = [
{
title: "Track issues effectively",
title: 'Track issues effectively',
description:
"Track and manage your project issues with ease using our intuitive interface.",
'Track and manage your project issues with ease using our intuitive interface.',
skeleton: <SkeletonOne />,
className:
"col-span-1 lg:col-span-4 border-b lg:border-r dark:border-neutral-800",
'col-span-1 lg:col-span-4 border-b lg:border-r dark:border-neutral-800',
},
{
title: "Capture pictures with AI",
title: 'Capture pictures with AI',
description:
"Capture stunning photos effortlessly using our advanced AI technology.",
'Capture stunning photos effortlessly using our advanced AI technology.',
skeleton: <SkeletonTwo />,
className: "border-b col-span-1 lg:col-span-2 dark:border-neutral-800",
className:
'border-b col-span-1 lg:col-span-2 dark:border-neutral-800',
},
{
title: "Watch our AI on YouTube",
title: 'Watch our AI on YouTube',
description:
"Whether its you or Tyler Durden, you can get to know about our product on YouTube",
'Whether its you or Tyler Durden, you can get to know about our product on YouTube',
skeleton: <SkeletonThree />,
className:
"col-span-1 lg:col-span-3 lg:border-r dark:border-neutral-800",
'col-span-1 lg:col-span-3 lg:border-r dark:border-neutral-800',
},
{
title: "Deploy in seconds",
title: 'Deploy in seconds',
description:
"With our blazing fast, state of the art, cutting edge, we are so back cloud servies (read AWS) - you can deploy your model in seconds.",
'With our blazing fast, state of the art, cutting edge, we are so back cloud servies (read AWS) - you can deploy your model in seconds.',
skeleton: <SkeletonFour />,
className: "col-span-1 lg:col-span-3 border-b lg:border-none",
className: 'col-span-1 lg:col-span-3 border-b lg:border-none',
},
];
]
return (
<div className="relative z-20 py-10 lg:py-40 max-w-7xl mx-auto">
<div className="relative z-20 mx-auto max-w-7xl py-10 lg:py-40">
<div className="px-8">
<h4 className="text-3xl lg:text-5xl lg:leading-tight max-w-5xl mx-auto text-center tracking-tight font-medium text-black dark:text-white">
<h4 className="mx-auto max-w-5xl text-center text-3xl font-medium tracking-tight text-black dark:text-white lg:text-5xl lg:leading-tight">
Packed with thousands of features
</h4>
<p className="text-sm lg:text-base max-w-2xl my-4 mx-auto text-neutral-500 text-center font-normal dark:text-neutral-300">
From Image generation to video generation, Everything AI has APIs for
literally everything. It can even create this website copy for you.
<p className="mx-auto my-4 max-w-2xl text-center text-sm font-normal text-neutral-500 dark:text-neutral-300 lg:text-base">
From Image generation to video generation, Everything AI has
APIs for literally everything. It can even create this
website copy for you.
</p>
</div>
<div className="relative ">
<div className="grid grid-cols-1 lg:grid-cols-6 mt-12 xl:border rounded-md dark:border-neutral-800">
<div className="mt-12 grid grid-cols-1 rounded-md dark:border-neutral-800 lg:grid-cols-6 xl:border">
{features.map((feature) => (
<FeatureCard key={feature.title} className={feature.className}>
<FeatureCard
key={feature.title}
className={feature.className}
>
<FeatureTitle>{feature.title}</FeatureTitle>
<FeatureDescription>{feature.description}</FeatureDescription>
<div className=" h-full w-full">{feature.skeleton}</div>
<FeatureDescription>
{feature.description}
</FeatureDescription>
<div className=" h-full w-full">
{feature.skeleton}
</div>
</FeatureCard>
))}
</div>
</div>
</div>
);
)
}
const FeatureCard = ({
children,
className,
}: {
children?: React.ReactNode;
className?: string;
children?: React.ReactNode
className?: string
}) => {
return (
<div className={cn("p-4 sm:p-8 relative overflow-hidden", className)}>
<div className={cn('relative overflow-hidden p-4 sm:p-8', className)}>
{children}
</div>
);
};
)
}
const FeatureTitle = ({ children }: { children?: React.ReactNode }) => {
return (
<p className=" max-w-5xl mx-auto text-left tracking-tight text-black dark:text-white text-xl md:text-2xl md:leading-snug">
<p className=" mx-auto max-w-5xl text-left text-xl tracking-tight text-black dark:text-white md:text-2xl md:leading-snug">
{children}
</p>
);
};
)
}
const FeatureDescription = ({ children }: { children?: React.ReactNode }) => {
return (
<p
className={cn(
"text-sm md:text-base max-w-4xl text-left mx-auto",
"text-neutral-500 text-center font-normal dark:text-neutral-300",
"text-left max-w-sm mx-0 md:text-sm my-2",
'mx-auto max-w-4xl text-left text-sm md:text-base',
'text-center font-normal text-neutral-500 dark:text-neutral-300',
'mx-0 my-2 max-w-sm text-left md:text-sm',
)}
>
{children}
</p>
);
};
)
}
export const SkeletonOne = () => {
return (
<div className="relative flex py-8 px-2 gap-10 h-full">
<div className="w-full p-5 mx-auto bg-white dark:bg-neutral-900 shadow-2xl group h-full">
<div className="flex flex-1 w-full h-full flex-col space-y-2 ">
<div className="relative flex h-full gap-10 px-2 py-8">
<div className="group mx-auto h-full w-full bg-white p-5 shadow-2xl dark:bg-neutral-900">
<div className="flex h-full w-full flex-1 flex-col space-y-2 ">
{/* TODO */}
<Image
src="/linear.webp"
alt="header"
width={800}
height={800}
className="h-full w-full aspect-square object-cover object-left-top rounded-sm"
className="aspect-square h-full w-full rounded-sm object-cover object-left-top"
/>
</div>
</div>
<div className="absolute bottom-0 z-40 inset-x-0 h-60 bg-gradient-to-t from-white dark:from-black via-white dark:via-black to-transparent w-full pointer-events-none" />
<div className="absolute top-0 z-40 inset-x-0 h-60 bg-gradient-to-b from-white dark:from-black via-transparent to-transparent w-full pointer-events-none" />
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-40 h-60 w-full bg-gradient-to-t from-white via-white to-transparent dark:from-black dark:via-black" />
<div className="pointer-events-none absolute inset-x-0 top-0 z-40 h-60 w-full bg-gradient-to-b from-white via-transparent to-transparent dark:from-black" />
</div>
);
};
)
}
export const SkeletonThree = () => {
return (
<Link
href="https://www.youtube.com/watch?v=RPa3_AD1_Vs"
target="__blank"
className="relative flex gap-10 h-full group/image"
className="group/image relative flex h-full gap-10"
>
<div className="w-full mx-auto bg-transparent dark:bg-transparent group h-full">
<div className="flex flex-1 w-full h-full flex-col space-y-2 relative">
<div className="group mx-auto h-full w-full bg-transparent dark:bg-transparent">
<div className="relative flex h-full w-full flex-1 flex-col space-y-2">
{/* TODO */}
<IconBrandYoutubeFilled className="h-20 w-20 absolute z-10 inset-0 text-red-500 m-auto " />
<IconBrandYoutubeFilled className="absolute inset-0 z-10 m-auto h-20 w-20 text-red-500 " />
<Image
src="https://assets.aceternity.com/fireship.jpg"
alt="header"
width={800}
height={800}
className="h-full w-full aspect-square object-cover object-center rounded-sm blur-none group-hover/image:blur-md transition-all duration-200"
className="aspect-square h-full w-full rounded-sm object-cover object-center blur-none transition-all duration-200 group-hover/image:blur-md"
/>
</div>
</div>
</Link>
);
};
)
}
export const SkeletonTwo = () => {
const images = [
"https://images.unsplash.com/photo-1517322048670-4fba75cbbb62?q=80&w=3000&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1573790387438-4da905039392?q=80&w=3425&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1555400038-63f5ba517a47?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1554931670-4ebfabf6e7a9?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1546484475-7f7bd55792da?q=80&w=2581&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
];
'https://images.unsplash.com/photo-1517322048670-4fba75cbbb62?q=80&w=3000&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1573790387438-4da905039392?q=80&w=3425&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1555400038-63f5ba517a47?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1554931670-4ebfabf6e7a9?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1546484475-7f7bd55792da?q=80&w=2581&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
]
const imageVariants = {
whileHover: {
@@ -168,11 +177,11 @@ export const SkeletonTwo = () => {
rotate: 0,
zIndex: 100,
},
};
}
return (
<div className="relative flex flex-col items-start p-8 gap-10 h-full overflow-hidden">
<div className="relative flex h-full flex-col items-start gap-10 overflow-hidden p-8">
{/* TODO */}
<div className="flex flex-row -ml-20">
<div className="-ml-20 flex flex-row">
{images.map((image, idx) => (
<motion.div
variants={imageVariants}
@@ -182,14 +191,14 @@ export const SkeletonTwo = () => {
}}
whileHover="whileHover"
whileTap="whileTap"
className="rounded-xl -mr-4 mt-4 p-1 bg-white dark:bg-neutral-800 dark:border-neutral-700 border border-neutral-100 flex-shrink-0 overflow-hidden"
className="-mr-4 mt-4 flex-shrink-0 overflow-hidden rounded-xl border border-neutral-100 bg-white p-1 dark:border-neutral-700 dark:bg-neutral-800"
>
<Image
src={image}
alt="bali images"
width="500"
height="500"
className="rounded-lg h-20 w-20 md:h-40 md:w-40 object-cover flex-shrink-0"
className="h-20 w-20 flex-shrink-0 rounded-lg object-cover md:h-40 md:w-40"
/>
</motion.div>
))}
@@ -204,29 +213,29 @@ export const SkeletonTwo = () => {
variants={imageVariants}
whileHover="whileHover"
whileTap="whileTap"
className="rounded-xl -mr-4 mt-4 p-1 bg-white dark:bg-neutral-800 dark:border-neutral-700 border border-neutral-100 flex-shrink-0 overflow-hidden"
className="-mr-4 mt-4 flex-shrink-0 overflow-hidden rounded-xl border border-neutral-100 bg-white p-1 dark:border-neutral-700 dark:bg-neutral-800"
>
<Image
src={image}
alt="bali images"
width="500"
height="500"
className="rounded-lg h-20 w-20 md:h-40 md:w-40 object-cover flex-shrink-0"
className="h-20 w-20 flex-shrink-0 rounded-lg object-cover md:h-40 md:w-40"
/>
</motion.div>
))}
</div>
<div className="absolute left-0 z-[100] inset-y-0 w-20 bg-gradient-to-r from-white dark:from-black to-transparent h-full pointer-events-none" />
<div className="absolute right-0 z-[100] inset-y-0 w-20 bg-gradient-to-l from-white dark:from-black to-transparent h-full pointer-events-none" />
<div className="pointer-events-none absolute inset-y-0 left-0 z-[100] h-full w-20 bg-gradient-to-r from-white to-transparent dark:from-black" />
<div className="pointer-events-none absolute inset-y-0 right-0 z-[100] h-full w-20 bg-gradient-to-l from-white to-transparent dark:from-black" />
</div>
);
};
)
}
export const SkeletonFour = () => {
return (
<div className="h-60 md:h-60 flex flex-col items-center relative bg-transparent dark:bg-transparent mt-10">
<div className="relative mt-10 flex h-60 flex-col items-center bg-transparent dark:bg-transparent md:h-60">
{/* <Globe className="absolute -right-10 md:-right-10 -bottom-80 md:-bottom-72" /> */}
</div>
);
};
)
}

View File

@@ -1,5 +1,5 @@
"use client";
import { cn } from "@/lib/utils";
'use client'
import { cn } from '@/lib/utils'
import {
IconActivity,
IconCloud,
@@ -10,101 +10,101 @@ import {
IconTerminal,
IconTerminal2,
IconUsers,
} from "@tabler/icons-react";
import { Layers, Lock, UnlockIcon } from "lucide-react";
} from '@tabler/icons-react'
import { Layers, Lock, UnlockIcon } from 'lucide-react'
export function FirstFeaturesSection() {
const features = [
{
title: "Flexible Application Deployment",
title: 'Flexible Application Deployment',
description:
"Deploy any application using Nixpacks, Heroku Buildpacks, or your custom Dockerfile, tailored to your stack.",
'Deploy any application using Nixpacks, Heroku Buildpacks, or your custom Dockerfile, tailored to your stack.',
icon: <IconRocket />,
},
{
title: "Native Docker Compose Support",
title: 'Native Docker Compose Support',
description:
"Deploy complex applications natively with full Docker Compose integration for seamless orchestration.",
'Deploy complex applications natively with full Docker Compose integration for seamless orchestration.',
icon: <Layers />,
},
{
title: "Multi-server Support",
title: 'Multi-server Support',
description:
"Effortlessly deploy your applications on remote servers, with zero configuration hassle.",
'Effortlessly deploy your applications on remote servers, with zero configuration hassle.',
icon: <IconCloud />,
},
{
title: "Advanced User Management",
title: 'Advanced User Management',
description:
"Control user access with detailed roles and permissions, keeping your deployments secure and organized.",
'Control user access with detailed roles and permissions, keeping your deployments secure and organized.',
icon: <IconUsers />,
},
{
title: "Database Management with Backups",
title: 'Database Management with Backups',
description:
"Manage and back up MySQL, PostgreSQL, MongoDB, MariaDB, Redis directly from Dokploy.",
'Manage and back up MySQL, PostgreSQL, MongoDB, MariaDB, Redis directly from Dokploy.',
icon: <IconDatabase />,
},
{
title: "API & CLI Access",
title: 'API & CLI Access',
description:
"Need custom functionality? Dokploy offers complete API and CLI access to fit your needs.",
'Need custom functionality? Dokploy offers complete API and CLI access to fit your needs.',
icon: <IconTerminal />,
},
{
title: "Docker Swarm Clusters",
title: 'Docker Swarm Clusters',
description:
"Scale your deployments seamlessly with built-in Docker Swarm support for robust, multi-node applications.",
'Scale your deployments seamlessly with built-in Docker Swarm support for robust, multi-node applications.',
icon: <IconUsers />,
},
{
title: "Open Source Templates",
title: 'Open Source Templates',
description:
"Get started quickly with pre-configured templates for popular tools like Supabase, Cal.com, and PocketBase.",
'Get started quickly with pre-configured templates for popular tools like Supabase, Cal.com, and PocketBase.',
icon: <IconTemplate />,
},
{
title: "No Vendor Lock-In",
title: 'No Vendor Lock-In',
description:
"Experience complete freedom to modify, scale, and customize Dokploy to suit your specific needs.",
'Experience complete freedom to modify, scale, and customize Dokploy to suit your specific needs.',
icon: <UnlockIcon />,
},
{
title: "Real-time Monitoring & Alerts",
title: 'Real-time Monitoring & Alerts',
description:
"Monitor CPU, memory, and network usage in real-time across your deployments for full visibility.",
'Monitor CPU, memory, and network usage in real-time across your deployments for full visibility.',
icon: <IconActivity />,
},
{
title: "Built for Developers",
title: 'Built for Developers',
description:
"Designed specifically for engineers and developers seeking control and flexibility.",
'Designed specifically for engineers and developers seeking control and flexibility.',
icon: <IconTerminal2 />,
},
{
title: "Self-hosted & Open Source",
title: 'Self-hosted & Open Source',
description:
"Dokploy provides complete control with self-hosting capabilities and open-source transparency.",
'Dokploy provides complete control with self-hosting capabilities and open-source transparency.',
icon: <IconEaseInOut />,
},
];
]
return (
<div className="flex flex-col justify-center items-center mt-20 px-4">
<h2 className="font-display text-3xl tracking-tight text-primary sm:text-4xl text-center">
<div className="mt-20 flex flex-col items-center justify-center px-4">
<h2 className="text-center font-display text-3xl tracking-tight text-primary sm:text-4xl">
Powerful Deployment Tailored to You
</h2>
<p className="mt-4 text-lg tracking-tight text-muted-foreground text-center">
Unlock seamless multi-server deployments, advanced user control, and
flexible database managementall with Dokploys developer-focused
features.
<p className="mt-4 text-center text-lg tracking-tight text-muted-foreground">
Unlock seamless multi-server deployments, advanced user control,
and flexible database managementall with Dokploys
developer-focused features.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 relative z-10 py-10 max-w-7xl mx-auto mt-10 max-sm:p-0 max-sm:mx-0 max-sm:w-full">
<div className="relative z-10 mx-auto mt-10 grid max-w-7xl grid-cols-1 py-10 max-sm:mx-0 max-sm:w-full max-sm:p-0 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{features.map((feature, index) => (
<Feature key={feature.title} {...feature} index={index} />
))}
</div>
</div>
);
)
}
const Feature = ({
@@ -113,36 +113,39 @@ const Feature = ({
icon,
index,
}: {
title: string;
description: string;
icon: React.ReactNode;
index: number;
title: string
description: string
icon: React.ReactNode
index: number
}) => {
return (
<div
className={cn(
"flex flex-col lg:border-r py-10 relative group/feature border-neutral-800",
'group/feature relative flex flex-col border-neutral-800 py-10 lg:border-r',
(index === 0 || index === 4 || index === 8) &&
"lg:border-l dark:border-neutral-800",
(index < 4 || index < 8) && "lg:border-b dark:border-neutral-800",
'dark:border-neutral-800 lg:border-l',
(index < 4 || index < 8) &&
'dark:border-neutral-800 lg:border-b',
)}
>
{index < 4 && (
<div className="opacity-0 group-hover/feature:opacity-100 transition duration-200 absolute inset-0 h-full w-full bg-gradient-to-t from-neutral-800 to-transparent pointer-events-none" />
<div className="pointer-events-none absolute inset-0 h-full w-full bg-gradient-to-t from-neutral-800 to-transparent opacity-0 transition duration-200 group-hover/feature:opacity-100" />
)}
{index >= 4 && (
<div className="opacity-0 group-hover/feature:opacity-100 transition duration-200 absolute inset-0 h-full w-full bg-gradient-to-b from-neutral-800 to-transparent pointer-events-none" />
<div className="pointer-events-none absolute inset-0 h-full w-full bg-gradient-to-b from-neutral-800 to-transparent opacity-0 transition duration-200 group-hover/feature:opacity-100" />
)}
<div className="mb-4 relative z-10 px-10 text-neutral-400">{icon}</div>
<div className="text-lg font-bold mb-2 relative z-10 px-10">
<div className="absolute left-0 inset-y-0 h-6 group-hover/feature:h-8 w-1 rounded-tr-full rounded-br-full bg-neutral-700 group-hover/feature:bg-white transition-all duration-200 origin-center" />
<span className="group-hover/feature:translate-x-2 transition duration-200 inline-block text-neutral-100">
<div className="relative z-10 mb-4 px-10 text-neutral-400">
{icon}
</div>
<div className="relative z-10 mb-2 px-10 text-lg font-bold">
<div className="absolute inset-y-0 left-0 h-6 w-1 origin-center rounded-br-full rounded-tr-full bg-neutral-700 transition-all duration-200 group-hover/feature:h-8 group-hover/feature:bg-white" />
<span className="inline-block text-neutral-100 transition duration-200 group-hover/feature:translate-x-2">
{title}
</span>
</div>
<p className="text-sm text-neutral-300 lg:max-w-xs relative z-10 px-10">
<p className="relative z-10 px-10 text-sm text-neutral-300 lg:max-w-xs">
{description}
</p>
</div>
);
};
)
}

View File

@@ -1,6 +1,6 @@
const navigation = [
{ name: "home", href: "/" },
{ name: "features", href: "/features" },
{ name: "pricing", href: "/pricing" },
{ name: "blog", href: "/blog" },
];
{ name: 'home', href: '/' },
{ name: 'features', href: '/features' },
{ name: 'pricing', href: '/pricing' },
{ name: 'blog', href: '/blog' },
]

File diff suppressed because one or more lines are too long

View File

@@ -1,80 +1,87 @@
"use client";
'use client'
import { Tab } from "@headlessui/react";
import { AnimatePresence, motion } from "framer-motion";
import { useEffect, useState } from "react";
import { Tab } from '@headlessui/react'
import { AnimatePresence, motion } from 'framer-motion'
import { useEffect, useState } from 'react'
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils'
const features = [
{
title: "Applications & Databases",
description: "Centralize control of your applications and databases for enhanced security and efficiency, simplifying access and management across your infrastructure.",
image: "/dashboard.png",
title: 'Applications & Databases',
description:
'Centralize control of your applications and databases for enhanced security and efficiency, simplifying access and management across your infrastructure.',
image: '/dashboard.png',
},
{
title: "Docker Compose",
description: "Native Docker Compose support so you can manage complex applications and services with ease.",
image: "/compose.png",
title: 'Docker Compose',
description:
'Native Docker Compose support so you can manage complex applications and services with ease.',
image: '/compose.png',
},
{
title: "Multiserver",
description: "Deploy applications to multiple servers without the extra effort.",
image: "/remote.png",
title: 'Multiserver',
description:
'Deploy applications to multiple servers without the extra effort.',
image: '/remote.png',
},
{
title: "Logs",
description: "Monitor and manage your applications' logs with ease, ensuring efficient troubleshooting and optimal performance.",
image: "/logs.png",
title: 'Logs',
description:
"Monitor and manage your applications' logs with ease, ensuring efficient troubleshooting and optimal performance.",
image: '/logs.png',
},
{
title: "Monitoring",
description: "Monitor your systems' performance and health in real time, ensuring continuous and uninterrupted operation.",
image: "/primary/monitoring.png",
title: 'Monitoring',
description:
"Monitor your systems' performance and health in real time, ensuring continuous and uninterrupted operation.",
image: '/primary/monitoring.png',
},
{
title: "Backups",
description: "Implement automatic and secure backup solutions to protect your critical data and restore it quickly when necessary.",
image: "/backups.png",
title: 'Backups',
description:
'Implement automatic and secure backup solutions to protect your critical data and restore it quickly when necessary.',
image: '/backups.png',
},
{
title: "Traefik",
description: "Manage Traefik via File Editor to configure your own domain names, certificates, and more.",
image: "/traefik.png",
title: 'Traefik',
description:
'Manage Traefik via File Editor to configure your own domain names, certificates, and more.',
image: '/traefik.png',
},
{
title: "Templates",
description: "Deploy open source templates with one click.",
image: "/templates.png",
title: 'Templates',
description: 'Deploy open source templates with one click.',
image: '/templates.png',
},
];
]
export function SecondaryFeaturesSections() {
const [tabOrientation, setTabOrientation] = useState<
"horizontal" | "vertical"
>("horizontal");
'horizontal' | 'vertical'
>('horizontal')
useEffect(() => {
const lgMediaQuery = window.matchMedia("(min-width: 1024px)");
const lgMediaQuery = window.matchMedia('(min-width: 1024px)')
function onMediaQueryChange({ matches }: { matches: boolean }) {
setTabOrientation(matches ? "vertical" : "horizontal");
setTabOrientation(matches ? 'vertical' : 'horizontal')
}
onMediaQueryChange(lgMediaQuery);
lgMediaQuery.addEventListener("change", onMediaQueryChange);
onMediaQueryChange(lgMediaQuery)
lgMediaQuery.addEventListener('change', onMediaQueryChange)
return () => {
lgMediaQuery.removeEventListener("change", onMediaQueryChange);
};
}, []);
lgMediaQuery.removeEventListener('change', onMediaQueryChange)
}
}, [])
const [isMounted, setIsMounted] = useState(false);
const [isMounted, setIsMounted] = useState(false)
// Cambiar isMounted a true después del primer render
useEffect(() => {
setIsMounted(true);
}, []);
setIsMounted(true)
}, [])
return (
<section
@@ -82,13 +89,15 @@ export function SecondaryFeaturesSections() {
aria-label="Features for running your books"
className="relative overflow-hidden bg-black pb-28 pt-20 sm:py-32"
>
<div className="mx-auto max-w-7xl max-lg:px-4 relative">
<div className="relative mx-auto max-w-7xl max-lg:px-4">
<div className="max-w-2xl md:mx-auto md:text-center xl:max-w-none">
<h2 className="font-display text-3xl tracking-tight text-white sm:text-4xl md:text-5xl">
Comprehensive Control of Your Digital Ecosystem
</h2>
<p className="mt-6 text-lg tracking-tight text-muted-foreground">
Simplify your project and data management, ensure robust monitoring, and secure your backupsall without the fuss over minute details.
Simplify your project and data management, ensure robust
monitoring, and secure your backupsall without the fuss
over minute details.
</p>
</div>
<Tab.Group
@@ -98,7 +107,7 @@ export function SecondaryFeaturesSections() {
>
{({ selectedIndex }) => (
<>
<div className="-mx-4 flex overflow-x-auto pb-4 sm:mx-0 overflow-visible sm:pb-0">
<div className="-mx-4 flex overflow-visible overflow-x-auto pb-4 sm:mx-0 sm:pb-0">
<Tab.List
aria-description="primary feature tabs"
aria-roledescription="primary feature tabs"
@@ -110,11 +119,12 @@ export function SecondaryFeaturesSections() {
initial={false}
key={`feature-${featureIndex}`}
className={cn(
"group relative rounded-full px-4 py-1 transition-colors ",
'group relative rounded-full px-4 py-1 transition-colors ',
)}
>
<AnimatePresence>
{selectedIndex === featureIndex && (
{selectedIndex ===
featureIndex && (
<motion.span
layoutId="tab"
className="absolute inset-0 z-10 rounded-full bg-white/5 mix-blend-difference"
@@ -122,7 +132,7 @@ export function SecondaryFeaturesSections() {
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
type: "spring",
type: 'spring',
bounce: 0.2,
duration: 0.5,
}}
@@ -132,7 +142,7 @@ export function SecondaryFeaturesSections() {
<h3>
<Tab
className={cn(
"font-display text-lg text-primary ui-not-focus-visible:outline-none",
'font-display text-lg text-primary ui-not-focus-visible:outline-none',
)}
>
<span className="absolute inset-0 rounded-full" />
@@ -141,7 +151,7 @@ export function SecondaryFeaturesSections() {
</h3>
<p
className={cn(
"mt-2 hidden text-sm text-muted-foreground ",
'mt-2 hidden text-sm text-muted-foreground ',
)}
>
{feature.description}
@@ -155,18 +165,24 @@ export function SecondaryFeaturesSections() {
<Tab.Panel key={`panel-${index}`}>
<div className="relative sm:px-6 ">
<div className="absolute -inset-x-4 bottom-[-4.25rem] top-[-6.5rem] bg-card/60 ring-1 ring-inset ring-white/10 sm:inset-x-0 sm:rounded-t-xl" />
<p className="relative mx-auto max-w-2xl text-base text-white sm:text-center mb-10">
<p className="relative mx-auto mb-10 max-w-2xl text-base text-white sm:text-center">
{feature.description}
</p>
</div>
<motion.div
key={feature.title}
initial={isMounted ? { opacity: 0.4 } : {}}
animate={isMounted ? { opacity: 1 } : {}}
initial={
isMounted
? { opacity: 0.4 }
: {}
}
animate={
isMounted ? { opacity: 1 } : {}
}
exit={{ opacity: 0, x: -50 }}
transition={{
type: "spring",
type: 'spring',
bounce: 0.2,
duration: 0.8,
}}
@@ -174,13 +190,16 @@ export function SecondaryFeaturesSections() {
>
<div className="relative w-full">
<div className="mx-auto">
<div className="w-full h-11 rounded-t-lg bg-card flex justify-start items-center space-x-1.5 px-3">
<span className="w-3 h-3 rounded-full bg-red-400" />
<span className="w-3 h-3 rounded-full bg-yellow-400" />
<span className="w-3 h-3 rounded-full bg-green-400" />
<div className="flex h-11 w-full items-center justify-start space-x-1.5 rounded-t-lg bg-card px-3">
<span className="h-3 w-3 rounded-full bg-red-400" />
<span className="h-3 w-3 rounded-full bg-yellow-400" />
<span className="h-3 w-3 rounded-full bg-green-400" />
</div>
<div className="bg-gray-100 w-full h-96">
<img src={feature.image} alt={feature.title} />
<div className="h-96 w-full bg-gray-100">
<img
src={feature.image}
alt={feature.title}
/>
</div>
</div>
</div>
@@ -193,5 +212,5 @@ export function SecondaryFeaturesSections() {
</Tab.Group>
</div>
</section>
);
)
}

View File

@@ -1,4 +1,4 @@
export function Logo(props: React.ComponentPropsWithoutRef<"svg">) {
export function Logo(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -21,5 +21,5 @@ export function Logo(props: React.ComponentPropsWithoutRef<"svg">) {
d="M465 211h-1c-18.2 14.6-41.2 24.6-60 39-19 14.2-42.7 29.3-66 34l-4 1c-2.4 1-4 2-7 2h-1q-3.5 2-8 2h-1c-1.3 1.2-3 1.1-5 1h-2q-2.6 1.1-6 1h-2c-3 1.2-6.5 1-10 1-6.3.6-13.8.6-20 0-3.4 0-8.4.9-11-1h-1c-2.2.2-4.5.3-6-1h-1c-2 .2-3.7.2-5-1h-1c-7.6.5-16.5-3.4-23-6l-4-1a129 129 0 0 1-36.2-15.8c-10.4-6.6-23.2-12.8-32.5-20.5-9.2-7.7-23.8-12.8-30.3-22.7h-1c-2.3-1.4-4.5-2.7-6-5h-1c-4-2.5-8.5-5.2-12-8h-9a9 9 0 0 0-6 7c.3 3.3 0 6.7 0 10v9c.2 1.6 1 3.8 1 6v3c.2 1 1.2 2.2 1 4v1c1.2 1.2.8 2.2 1 4 .8 6.7 3 12.6 5 19 1.7 4.3 4.2 9.1 5 14v1q1.8 1.5 2 4v1a36 36 0 0 1 5 10c.7 2 1 3 2 5 8 12.7 15.7 25.5 25.8 37.3 10 11.7 20.8 20.6 32.4 30.4 11.7 9.9 28.3 14 39.8 23.3h1q2.5.3 4 2h1c2.8.4 4.8 2 7 3l7 2c5.7 1.3 13 2.3 18 5h1c2.1-.3 3.6.8 5 1h3c2.8.2 5.8 1 8 2h8c2.1 0 4.6.8 6 1h21c1.2-.2 3.2-1 5-1h9c3.3-1 7-2.4 11-2h1c2.7-2.2 7.4-2.4 11-3a55 55 0 0 0 8-2c6.5-2.6 13.9-6.3 21-8h1c8.5-6.8 20.6-9.7 29.2-16.8 8.7-7 18.3-12.8 26.8-20.2 4.4-3.8 9-9 13-13 14.8-14.8 20.7-34.6 33-50v-1q.9-3.4 3-6v-1q.3-2.5 2-4v-1c.5-3.3 2-8.6 4-11v-1q0-3.5 2-6v-1c1.1-6.7 2.4-15 5-21v-1c-.2-2-.2-3.7 1-5v-8c0-5.3-.5-10.8 0-16a14 14 0 0 0-4-6c-1-.5-1.1-.4-2-1h-6q-2.1 1.5-5 2m-6 38c-2.1 13.4-21.2 20.3-31 30-10 9.5-23.7 19-35 27-11.5 8-25.1 19.7-39 23h-1a22 22 0 0 1-10 4h-1a25 25 0 0 1-12 4h-1q-3.5 2-8 2h-1c-1.1 1.1-2.3 1-4 1h-2c-1.2.4-2.2 1-4 1h-2c-1.8.7-3.6 1.3-6 1h-1c-1.2 1.2-2.3 1-4 1h-5c-5.7.6-12.3.8-18 0h-4c-1.9 0-2.7-.6-4-1h-6c-1.9 0-2.7.3-4-1h-1q-2.5.5-4-1h-1c-8.1.5-16.8-3.6-24.2-5.8S210 329.8 204 325h-1c-12.8-5-27.1-15.6-37.7-24.3S138.8 284.2 131 273c-.3-.2-1 0-1 0-5.7-4.4-16.6-10-19-17-.9-2.6-1-5.4-2-8-.8-2.2-2.5-5-2-8a667 667 0 0 0 88 56h1q3.4.9 6 3h1c2.8.4 4.8 2 7 3q5 1.8 10 3l6 2q2.9.6 6 1 3 .4 5 1c1.6-.2 2 0 3 1h1c2-.2 3.7-.2 5 1h1c2.2-.3 3.4.4 5 1h8c1.6 0 3 .9 4 1h40c1.8-1.3 4.6-1.2 7-1h1c1.2-1.2 3.2-1.2 5-1h1c1.2-1.2 3.2-1.2 5-1h1c1.1-1.1 2.3-1 4-1h2c3.5-1.7 6.9-2.3 11-3l4-1c3.4-1.4 7.1-3 11-4 1.5-.4 2.5-.5 4-1 1.4-.7 2-1.9 4-2h1q2.6-2.1 6-3h1c2.5-2 6-3.8 9-5l3-1c1.4-.9 2-2.5 4-3h1q1.4-2.2 4-3h1c7.3-7.7 19-13.2 27.7-19.3 8.8-6.1 18.2-15 28.3-18.7.4-.2 1 0 1 0q3.8-3.9 9-6c1.3 2.5-.5 6.7-1 10m-20 55c-.2.4 0 1 0 1-3.4 9.6-12.7 19-19 27a88 88 0 0 1-12 12 214 214 0 0 1-26.7 20.3c-9.5 5.8-20 14.8-31.3 16.7h-1a22 22 0 0 1-10 4h-1c-3.2 2.6-8.9 3.3-13 4h-1q-1.5 1.4-4 1h-1q-1.5 1.4-4 1h-1c-4.9 2.3-10.5 1-16 2-1 .2-2.5 1-4 1-6.2.4-12.8.3-19 0-1.8 0-3.8-.8-5-1h-4c-1.6 0-3-.9-4-1h-4c-3.9-.3-8.8-1.3-12-3h-1c-3.3-.5-7.5-1-10-3h-1c-3.6-.1-8.4-1.8-11-4h-1c-3.9-.6-8-2.6-11-5h-1c-16.1-3.8-32.2-18.9-45-29a200 200 0 0 1-40-51c17.7 11.5 35 25.5 52 38h1c4 1.6 12.8 5.4 15 9h1c4.6 1 10.4 4.1 14 7h1q2.5.3 4 2h1c3.3.5 8.6 2 11 4h1q3.5 0 6 2h1q2.5-.5 4 1h1q2.5-.5 4 1h1c3.8-.2 7.9 1 11 2h9c1.6 0 3 .8 4 1h32c1.2-.2 3.2-1 5-1h8a139 139 0 0 1 20-4l5-1c2-.7 3.7-1.5 6-2l4-1c1.5-.6 3-1.7 5-2h1q3-2.4 7-3h1q2.6-2.1 6-3h1c11.7-9.4 27.6-14.6 39-25 11.6-10.3 25-18.5 37-28a15 15 0 0 1-5 10Z"
/>
</svg>
);
)
}

View File

@@ -1,24 +1,27 @@
"use client";
import { PlusCircleIcon } from "lucide-react";
import Link from "next/link";
import { buttonVariants } from "./ui/button";
import Ripple from "./ui/ripple";
'use client'
import { PlusCircleIcon } from 'lucide-react'
import Link from 'next/link'
import { buttonVariants } from './ui/button'
import Ripple from './ui/ripple'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./ui/tooltip";
} from './ui/tooltip'
export const Sponsors = () => {
return (
<div className="mt-20 flex flex-col justify-center gap-y-10 w-full ">
<div className="mt-20 flex w-full flex-col justify-center gap-y-10 ">
<div className="flex flex-col justify-start gap-4 px-4">
<h3 className="mx-auto max-w-2xl font-display text-3xl font-medium tracking-tight text-primary sm:text-5xl text-center">
<h3 className="mx-auto max-w-2xl text-center font-display text-3xl font-medium tracking-tight text-primary sm:text-5xl">
Sponsors
</h3>
<p className="mx-auto max-w-2xl text-lg tracking-tight text-muted-foreground text-center">
Dokploy is an open source project that is maintained by a community of volunteers. We would like to thank our sponsors for their support and contributions to the project, which help us to continue to develop and improve Dokploy.
<p className="mx-auto max-w-2xl text-center text-lg tracking-tight text-muted-foreground">
Dokploy is an open source project that is maintained by a
community of volunteers. We would like to thank our sponsors
for their support and contributions to the project, which
help us to continue to develop and improve Dokploy.
</p>
</div>
<div className="relative flex h-[700px] w-full flex-col items-center justify-center overflow-hidden bg-background md:shadow-xl">
@@ -26,18 +29,19 @@ export const Sponsors = () => {
<Tooltip>
<TooltipTrigger className="z-10 m-0 p-0">
<Link
href={"https://opencollective.com/dokploy"}
href={'https://opencollective.com/dokploy'}
target="_blank"
className={buttonVariants({
variant: "secondary",
size: "sm",
className: "bg-transparent !rounded-full w-fit !p-0 m-0",
variant: 'secondary',
size: 'sm',
className:
'm-0 w-fit !rounded-full bg-transparent !p-0',
})}
>
<PlusCircleIcon className="size-10 text-muted-foreground hover:text-primary transition-colors" />
<PlusCircleIcon className="size-10 text-muted-foreground transition-colors hover:text-primary" />
</Link>
</TooltipTrigger>
<TooltipContent className="bg-black rounded-lg border-0 text-center w-[200px] z-[200] text-white font-semibold">
<TooltipContent className="z-[200] w-[200px] rounded-lg border-0 bg-black text-center font-semibold text-white">
Become a sponsor 🤑
</TooltipContent>
</Tooltip>
@@ -45,5 +49,5 @@ export const Sponsors = () => {
<Ripple />
</div>
</div>
);
};
)
}

View File

@@ -1,81 +1,82 @@
"use client";
'use client'
import { HandCoins, Users } from "lucide-react";
import React, { useEffect, useState } from "react";
import { useId } from "react";
import NumberTicker from "./ui/number-ticker";
import { HandCoins, Users } from 'lucide-react'
import React, { useEffect, useState } from 'react'
import { useId } from 'react'
import NumberTicker from './ui/number-ticker'
const statsValues = {
githubStars: 26000,
dockerDownloads: 4000000,
contributors: 200,
sponsors: 50,
};
}
export function StatsSection() {
const [githubStars, setGithubStars] = useState(statsValues.githubStars);
const [githubStars, setGithubStars] = useState(statsValues.githubStars)
useEffect(() => {
const fetchGitHubStars = async () => {
try {
const response = await fetch(
"/api/github-stars?owner=dokploy&repo=dokploy",
);
'/api/github-stars?owner=dokploy&repo=dokploy',
)
if (response.ok) {
const data = await response.json();
setGithubStars(data.stargazers_count);
const data = await response.json()
setGithubStars(data.stargazers_count)
}
} catch (error) {
console.error("Error fetching GitHub stars:", error);
console.error('Error fetching GitHub stars:', error)
// Keep default value on error
}
};
}
fetchGitHubStars();
}, []);
fetchGitHubStars()
}, [])
return (
<div className="py-20 lg:py-40 flex flex-col gap-10 px-4 ">
<div className="flex flex-col gap-10 px-4 py-20 lg:py-40 ">
<div className="mx-auto max-w-2xl md:text-center">
<h2 className="font-display text-3xl tracking-tight sm:text-4xl text-center">
<h2 className="text-center font-display text-3xl tracking-tight sm:text-4xl">
Stats You Didn't Ask For (But Secretly Love to See)
</h2>
<p className="mt-4 text-lg tracking-tight text-muted-foreground text-center">
Just a few numbers to show we're not *completely* making this up.
Turns out, Dokploy has actually helped a few peoplewho knew?
<p className="mt-4 text-center text-lg tracking-tight text-muted-foreground">
Just a few numbers to show we're not *completely* making
this up. Turns out, Dokploy has actually helped a few
peoplewho knew?
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-10 md:gap-2 max-w-7xl mx-auto">
<div className="mx-auto grid max-w-7xl grid-cols-1 gap-10 sm:grid-cols-2 md:grid-cols-3 md:gap-2 lg:grid-cols-4">
{grid.map((feature, index) => (
<div
key={feature.title}
className="relative bg-gradient-to-b from-neutral-900 to-neutral-950 p-6 rounded-3xl overflow-hidden"
className="relative overflow-hidden rounded-3xl bg-gradient-to-b from-neutral-900 to-neutral-950 p-6"
>
<Grid size={20} />
<p className="text-base font-bold text-white relative z-20 flex flex-row gap-4 items-center">
<p className="relative z-20 flex flex-row items-center gap-4 text-base font-bold text-white">
{feature.title}
{feature.icon}
</p>
<p className="text-neutral-400 mt-4 text-base font-normal relative z-20">
{typeof feature.description === "function"
<p className="relative z-20 mt-4 text-base font-normal text-neutral-400">
{typeof feature.description === 'function'
? feature.description(githubStars)
: feature.description}
</p>
{typeof feature.component === "function"
{typeof feature.component === 'function'
? feature.component(githubStars)
: feature.component}
</div>
))}
</div>
</div>
);
)
}
const grid = [
{
title: "GitHub Stars",
title: 'GitHub Stars',
description: (stars: number) =>
`With over ${(stars / 1000).toFixed(1)}k stars on GitHub, Dokploy is trusted by developers worldwide. Explore our repositories and join our community!`,
icon: (
@@ -84,14 +85,14 @@ const grid = [
</svg>
),
component: (stars: number) => (
<p className="whitespace-pre-wrap text-2xl !font-semibold tracking-tighter mt-4">
<p className="mt-4 whitespace-pre-wrap text-2xl !font-semibold tracking-tighter">
<NumberTicker value={stars} />+
</p>
),
},
{
title: "DockerHub Downloads",
description: `Downloaded over ${(statsValues.dockerDownloads / 1000000).toFixed(2).split(".")[0]}M times, Dokploy has become a go-to solution for seamless deployments. Discover our presence on DockerHub.`,
title: 'DockerHub Downloads',
description: `Downloaded over ${(statsValues.dockerDownloads / 1000000).toFixed(2).split('.')[0]}M times, Dokploy has become a go-to solution for seamless deployments. Discover our presence on DockerHub.`,
icon: (
<svg
stroke="currentColor"
@@ -105,39 +106,39 @@ const grid = [
</svg>
),
component: (
<p className="whitespace-pre-wrap text-2xl !font-semibold tracking-tighter mt-4">
<p className="mt-4 whitespace-pre-wrap text-2xl !font-semibold tracking-tighter">
<NumberTicker value={statsValues.dockerDownloads} />+
</p>
),
},
{
title: "Community Contributors",
title: 'Community Contributors',
description: `Thanks to our growing base of over ${statsValues.contributors} contributors, Dokploy continues to thrive, with valuable contributions from developers around the world.`,
icon: <Users className="h-6 w-6 stroke-white" />,
component: (
<p className="whitespace-pre-wrap text-2xl !font-semibold tracking-tighter mt-4">
<p className="mt-4 whitespace-pre-wrap text-2xl !font-semibold tracking-tighter">
<NumberTicker value={statsValues.contributors} />+
</p>
),
},
{
title: "Sponsors",
title: 'Sponsors',
description: `More than ${statsValues.sponsors} companies/individuals have sponsored Dokploy, ensuring a steady flow of support and resources. Join our community!`,
icon: <HandCoins className="h-6 w-6 stroke-white" />,
component: (
<p className="whitespace-pre-wrap text-2xl !font-semibold tracking-tighter mt-4">
<p className="mt-4 whitespace-pre-wrap text-2xl !font-semibold tracking-tighter">
<NumberTicker value={statsValues.sponsors} />+
</p>
),
},
];
]
export const Grid = ({
pattern,
size,
}: {
pattern?: number[][];
size?: number;
pattern?: number[][]
size?: number
}) => {
const p = pattern ?? [
[Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1],
@@ -145,25 +146,25 @@ export const Grid = ({
[Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1],
[Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1],
[Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1],
];
]
return (
<div className="pointer-events-none absolute left-1/2 top-0 -ml-20 -mt-2 h-full w-full [mask-image:linear-gradient(white,transparent)]">
<div className="absolute inset-0 bg-gradient-to-r [mask-image:radial-gradient(farthest-side_at_top,white,transparent)] from-zinc-900/30 to-zinc-900/30 opacity-100">
<div className="absolute inset-0 bg-gradient-to-r from-zinc-900/30 to-zinc-900/30 opacity-100 [mask-image:radial-gradient(farthest-side_at_top,white,transparent)]">
<GridPattern
width={size ?? 20}
height={size ?? 20}
x="-12"
y="4"
squares={p}
className="absolute inset-0 h-full w-full mix-blend-overlay fill-white/10 stroke-white/10 "
className="absolute inset-0 h-full w-full fill-white/10 stroke-white/10 mix-blend-overlay "
/>
</div>
</div>
);
};
)
}
export function GridPattern({ width, height, x, y, squares, ...props }: any) {
const patternId = useId();
const patternId = useId()
return (
<svg aria-hidden="true" {...props}>
@@ -200,5 +201,5 @@ export function GridPattern({ width, height, x, y, squares, ...props }: any) {
</svg>
)}
</svg>
);
)
}

View File

@@ -1,12 +1,12 @@
"use client";
'use client'
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown, Minus, PlusIcon } from "lucide-react";
import * as React from "react";
import * as AccordionPrimitive from '@radix-ui/react-accordion'
import { ChevronDown, Minus, PlusIcon } from 'lucide-react'
import * as React from 'react'
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils'
const Accordion = AccordionPrimitive.Root;
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
@@ -14,11 +14,11 @@ const AccordionItem = React.forwardRef<
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
className={cn('border-b', className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
))
AccordionItem.displayName = 'AccordionItem'
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
@@ -28,7 +28,7 @@ const AccordionTrigger = React.forwardRef<
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180 group",
'group flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
className,
)}
{...props}
@@ -38,8 +38,8 @@ const AccordionTrigger = React.forwardRef<
<Minus className="h-4 w-4 shrink-0 transition-transform duration-200 group-data-[state=closed]:hidden" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
@@ -50,12 +50,12 @@ const AccordionContent = React.forwardRef<
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0 text-muted-foreground", className)}>
<div className={cn('pb-4 pt-0 text-muted-foreground', className)}>
{children}
</div>
</AccordionPrimitive.Content>
));
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@@ -1,28 +1,28 @@
import type { ReactNode } from "react";
import type { ReactNode } from 'react'
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils'
export default function AnimatedGradientText({
children,
className,
}: {
children: ReactNode;
className?: string;
children: ReactNode
className?: string
}) {
return (
<div
className={cn(
"group relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-2xl px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#8fdfff1f] backdrop-blur-sm transition-shadow duration-500 ease-out [--bg-size:300%] hover:shadow-[inset_0_-5px_10px_#8fdfff3f] bg-black/40",
'group relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-2xl bg-black/40 px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#8fdfff1f] backdrop-blur-sm transition-shadow duration-500 ease-out [--bg-size:300%] hover:shadow-[inset_0_-5px_10px_#8fdfff3f]',
className,
)}
>
<div
className={
"absolute inset-0 block h-full w-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] p-[1px] ![mask-composite:subtract] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]"
'absolute inset-0 block h-full w-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] p-[1px] [border-radius:inherit] ![mask-composite:subtract] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]'
}
/>
{children}
</div>
);
)
}

View File

@@ -1,21 +1,21 @@
"use client";
'use client'
import { motion } from "framer-motion";
import { useEffect, useId, useRef, useState } from "react";
import { motion } from 'framer-motion'
import { useEffect, useId, useRef, useState } from 'react'
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils'
interface GridPatternProps {
width?: number;
height?: number;
x?: number;
y?: number;
strokeDasharray?: any;
numSquares?: number;
className?: string;
maxOpacity?: number;
duration?: number;
repeatDelay?: number;
width?: number
height?: number
x?: number
y?: number
strokeDasharray?: any
numSquares?: number
className?: string
maxOpacity?: number
duration?: number
repeatDelay?: number
}
export function GridPattern({
@@ -31,16 +31,16 @@ export function GridPattern({
repeatDelay = 0.5,
...props
}: GridPatternProps) {
const id = useId();
const containerRef = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const [squares, setSquares] = useState(() => generateSquares(numSquares));
const id = useId()
const containerRef = useRef(null)
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
const [squares, setSquares] = useState(() => generateSquares(numSquares))
function getPos() {
return [
Math.floor((Math.random() * dimensions.width) / width),
Math.floor((Math.random() * dimensions.height) / height),
];
]
}
// Adjust the generateSquares function to return objects with an id, x, and y
@@ -48,7 +48,7 @@ export function GridPattern({
return Array.from({ length: count }, (_, i) => ({
id: i,
pos: getPos(),
}));
}))
}
// Function to update a single square's position
@@ -62,15 +62,15 @@ export function GridPattern({
}
: sq,
),
);
};
)
}
// Update squares to animate in
useEffect(() => {
if (dimensions.width && dimensions.height) {
setSquares(generateSquares(numSquares));
setSquares(generateSquares(numSquares))
}
}, [dimensions, numSquares]);
}, [dimensions, numSquares])
// Resize observer to update container dimensions
useEffect(() => {
@@ -79,27 +79,27 @@ export function GridPattern({
setDimensions({
width: entry.contentRect.width,
height: entry.contentRect.height,
});
})
}
});
})
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
resizeObserver.observe(containerRef.current)
}
return () => {
if (containerRef.current) {
resizeObserver.unobserve(containerRef.current);
resizeObserver.unobserve(containerRef.current)
}
};
}, [containerRef]);
}
}, [containerRef])
return (
<svg
ref={containerRef}
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/10",
'pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/10',
className,
)}
{...props}
@@ -130,7 +130,7 @@ export function GridPattern({
duration,
repeat: 1,
delay: index * 0.1,
repeatType: "reverse",
repeatType: 'reverse',
}}
onAnimationComplete={() => updateSquarePosition(id)}
key={`${x}-${y}-${index}`}
@@ -144,7 +144,7 @@ export function GridPattern({
))}
</svg>
</svg>
);
)
}
export default GridPattern;
export default GridPattern

View File

@@ -1,11 +1,11 @@
import type { CSSProperties, FC, ReactNode } from "react";
import type { CSSProperties, FC, ReactNode } from 'react'
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils'
interface AnimatedShinyTextProps {
children: ReactNode;
className?: string;
shimmerWidth?: number;
children: ReactNode
className?: string
shimmerWidth?: number
}
const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
@@ -17,24 +17,24 @@ const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
<p
style={
{
"--shiny-width": `${shimmerWidth}px`,
'--shiny-width': `${shimmerWidth}px`,
} as CSSProperties
}
className={cn(
"mx-auto max-w-md text-neutral-600/70 dark:text-neutral-400/70",
'mx-auto max-w-md text-neutral-600/70 dark:text-neutral-400/70',
// Shine effect
"animate-shiny-text bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shiny-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]",
'animate-shiny-text bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shiny-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',
// Shine gradient
"bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80",
'bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80',
className,
)}
>
{children}
</p>
);
};
)
}
export default AnimatedShinyText;
export default AnimatedShinyText

View File

@@ -1,9 +1,9 @@
"use client";
'use client'
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as React from "react";
import * as AvatarPrimitive from '@radix-ui/react-avatar'
import * as React from 'react'
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils'
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
@@ -12,13 +12,13 @@ const Avatar = React.forwardRef<
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full',
className,
)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
@@ -26,11 +26,11 @@ const AvatarImage = React.forwardRef<
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
className={cn('aspect-square h-full w-full', className)}
{...props}
/>
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
@@ -39,12 +39,12 @@ const AvatarFallback = React.forwardRef<
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
'flex h-full w-full items-center justify-center rounded-full bg-muted',
className,
)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback };
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -1,27 +1,27 @@
import { type VariantProps, cva } from "class-variance-authority";
import type * as React from "react";
import { type VariantProps, cva } from 'class-variance-authority'
import type * as React from 'react'
import { cn } from "@/lib/utils";
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",
'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",
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
'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",
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: "default",
variant: 'default',
},
},
);
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
@@ -30,7 +30,7 @@ export interface BadgeProps
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
)
}
export { Badge, badgeVariants };
export { Badge, badgeVariants }

View File

@@ -1,59 +1,60 @@
"use client";
'use client'
import { Slot } from "@radix-ui/react-slot";
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
import { Slot } from '@radix-ui/react-slot'
import { type VariantProps, cva } from 'class-variance-authority'
import * as React from 'react'
import { cn } from "../../lib/utils";
import { cn } from '../../lib/utils'
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm will-change-transform transition-all active:hover:scale-[0.98] font-medium ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm will-change-transform transition-all active:hover:scale-[0.98] font-medium ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
default:
'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: "default",
size: "default",
variant: 'default',
size: 'default',
},
},
);
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
children?: React.ReactNode;
asChild?: boolean
children?: React.ReactNode
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
const Comp = asChild ? Slot : 'button'
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
)
},
);
Button.displayName = "Button";
)
Button.displayName = 'Button'
export { Button, buttonVariants };
export { Button, buttonVariants }

View File

@@ -1,20 +1,20 @@
"use client";
'use client'
import { CheckIcon, CopyIcon } from "lucide-react";
import { useState } from "react";
import { CheckIcon, CopyIcon } from 'lucide-react'
import { useState } from 'react'
interface CopyButtonProps {
text: string;
text: string
}
export function CopyButton({ text }: CopyButtonProps) {
const [isCopied, setIsCopied] = useState(false);
const [isCopied, setIsCopied] = useState(false)
const copy = async () => {
await navigator.clipboard.writeText(text);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
};
await navigator.clipboard.writeText(text)
setIsCopied(true)
setTimeout(() => setIsCopied(false), 2000)
}
return (
<button
@@ -28,5 +28,5 @@ export function CopyButton({ text }: CopyButtonProps) {
<CopyIcon className="h-full w-full text-gray-400" />
)}
</button>
);
)
}

View File

@@ -1,18 +1,18 @@
"use client";
'use client'
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import * as React from "react";
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { X } from 'lucide-react'
import * as React from 'react'
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils'
const Dialog = DialogPrimitive.Root;
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger;
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal;
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close;
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
@@ -21,13 +21,13 @@ const DialogOverlay = React.forwardRef<
<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",
'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;
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
@@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
<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",
'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}
@@ -50,8 +50,8 @@ const DialogContent = React.forwardRef<
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
@@ -59,13 +59,13 @@ const DialogHeader = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
'flex flex-col space-y-1.5 text-center sm:text-left',
className,
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
)
DialogHeader.displayName = 'DialogHeader'
const DialogFooter = ({
className,
@@ -73,13 +73,13 @@ const DialogFooter = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
)
DialogFooter.displayName = 'DialogFooter'
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
@@ -88,13 +88,13 @@ const DialogTitle = React.forwardRef<
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
'text-lg font-semibold leading-none tracking-tight',
className,
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
@@ -102,11 +102,11 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
@@ -119,4 +119,4 @@ export {
DialogFooter,
DialogTitle,
DialogDescription,
};
}

View File

@@ -1,86 +1,86 @@
"use client";
'use client'
import { AnimatePresence, motion } from "framer-motion";
import { Play, XIcon } from "lucide-react";
import { useState } from "react";
import { AnimatePresence, motion } from 'framer-motion'
import { Play, XIcon } from 'lucide-react'
import { useState } from 'react'
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils'
type AnimationStyle =
| "from-bottom"
| "from-center"
| "from-top"
| "from-left"
| "from-right"
| "fade"
| "top-in-bottom-out"
| "left-in-right-out";
| 'from-bottom'
| 'from-center'
| 'from-top'
| 'from-left'
| 'from-right'
| 'fade'
| 'top-in-bottom-out'
| 'left-in-right-out'
interface HeroVideoProps {
animationStyle?: AnimationStyle;
videoSrc: string;
thumbnailSrc: string;
thumbnailAlt?: string;
className?: string;
animationStyle?: AnimationStyle
videoSrc: string
thumbnailSrc: string
thumbnailAlt?: string
className?: string
}
const animationVariants = {
"from-bottom": {
initial: { y: "100%", opacity: 0 },
'from-bottom': {
initial: { y: '100%', opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: "100%", opacity: 0 },
exit: { y: '100%', opacity: 0 },
},
"from-center": {
'from-center': {
initial: { scale: 0.5, opacity: 0 },
animate: { scale: 1, opacity: 1 },
exit: { scale: 0.5, opacity: 0 },
},
"from-top": {
initial: { y: "-100%", opacity: 0 },
'from-top': {
initial: { y: '-100%', opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: "-100%", opacity: 0 },
exit: { y: '-100%', opacity: 0 },
},
"from-left": {
initial: { x: "-100%", opacity: 0 },
'from-left': {
initial: { x: '-100%', opacity: 0 },
animate: { x: 0, opacity: 1 },
exit: { x: "-100%", opacity: 0 },
exit: { x: '-100%', opacity: 0 },
},
"from-right": {
initial: { x: "100%", opacity: 0 },
'from-right': {
initial: { x: '100%', opacity: 0 },
animate: { x: 0, opacity: 1 },
exit: { x: "100%", opacity: 0 },
exit: { x: '100%', opacity: 0 },
},
fade: {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
},
"top-in-bottom-out": {
initial: { y: "-100%", opacity: 0 },
'top-in-bottom-out': {
initial: { y: '-100%', opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: "100%", opacity: 0 },
exit: { y: '100%', opacity: 0 },
},
"left-in-right-out": {
initial: { x: "-100%", opacity: 0 },
'left-in-right-out': {
initial: { x: '-100%', opacity: 0 },
animate: { x: 0, opacity: 1 },
exit: { x: "100%", opacity: 0 },
exit: { x: '100%', opacity: 0 },
},
};
}
export default function HeroVideoDialog({
animationStyle = "from-center",
animationStyle = 'from-center',
videoSrc,
thumbnailSrc,
thumbnailAlt = "Video thumbnail",
thumbnailAlt = 'Video thumbnail',
className,
}: HeroVideoProps) {
const [isVideoOpen, setIsVideoOpen] = useState(false);
const selectedAnimation = animationVariants[animationStyle];
const [isVideoOpen, setIsVideoOpen] = useState(false)
const selectedAnimation = animationVariants[animationStyle]
return (
<div className={cn("relative", className)}>
<div className={cn('relative', className)}>
<div
className="relative cursor-pointer group"
className="group relative cursor-pointer"
onClick={() => setIsVideoOpen(true)}
>
<img
@@ -88,20 +88,19 @@ export default function HeroVideoDialog({
alt={thumbnailAlt}
width={1920}
height={1080}
className="w-full transition-all duration-200 group-hover:brightness-[0.8] ease-out rounded-md shadow-lg border"
className="w-full rounded-md border shadow-lg transition-all duration-200 ease-out group-hover:brightness-[0.8]"
/>
<div className="absolute inset-0 flex items-center justify-center group-hover:scale-100 scale-[0.9] transition-all duration-200 ease-out rounded-2xl">
<div className="bg-primary/10 flex items-center justify-center rounded-full backdrop-blur-md size-28">
<div className="absolute inset-0 flex scale-[0.9] items-center justify-center rounded-2xl transition-all duration-200 ease-out group-hover:scale-100">
<div className="flex size-28 items-center justify-center rounded-full bg-primary/10 backdrop-blur-md">
<div
className={
"flex items-center justify-center bg-gradient-to-b from-primary/30 to-primary shadow-md rounded-full size-20 transition-all ease-out duration-200 relative group-hover:scale-[1.2] scale-100"
'relative flex size-20 scale-100 items-center justify-center rounded-full bg-gradient-to-b from-primary/30 to-primary shadow-md transition-all duration-200 ease-out group-hover:scale-[1.2]'
}
>
<Play
className="size-8 text-white fill-white group-hover:scale-105 scale-100 transition-transform duration-200 ease-out"
className="size-8 scale-100 fill-white text-white transition-transform duration-200 ease-out group-hover:scale-105"
style={{
filter:
"drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06))",
filter: 'drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06))',
}}
/>
</div>
@@ -119,13 +118,17 @@ export default function HeroVideoDialog({
>
<motion.div
{...selectedAnimation}
transition={{ type: "spring", damping: 30, stiffness: 300 }}
className="relative w-full max-w-4xl aspect-video mx-4 md:mx-0"
transition={{
type: 'spring',
damping: 30,
stiffness: 300,
}}
className="relative mx-4 aspect-video w-full max-w-4xl md:mx-0"
>
<motion.button className="absolute -top-16 right-0 text-white text-xl bg-neutral-900/50 ring-1 backdrop-blur-md rounded-full p-2 dark:bg-neutral-100/50 dark:text-black">
<motion.button className="absolute -top-16 right-0 rounded-full bg-neutral-900/50 p-2 text-xl text-white ring-1 backdrop-blur-md dark:bg-neutral-100/50 dark:text-black">
<XIcon className="size-5" />
</motion.button>
<div className="size-full border-2 border-white rounded-2xl overflow-hidden isolate z-[1] relative">
<div className="relative isolate z-[1] size-full overflow-hidden rounded-2xl border-2 border-white">
{/* biome-ignore lint/a11y/useIframeTitle: <explanation> */}
<iframe
src={videoSrc}
@@ -139,5 +142,5 @@ export default function HeroVideoDialog({
)}
</AnimatePresence>
</div>
);
)
}

View File

@@ -1,76 +1,74 @@
"use client";
import type React from "react";
import { useEffect, useRef, useState } from "react";
'use client'
import type React from 'react'
import { useEffect, useRef, useState } from 'react'
import { cn } from "@/lib/utils";
import { motion } from "framer-motion";
import { cn } from '@/lib/utils'
import { motion } from 'framer-motion'
type Direction = "TOP" | "LEFT" | "BOTTOM" | "RIGHT";
type Direction = 'TOP' | 'LEFT' | 'BOTTOM' | 'RIGHT'
export function HoverBorderGradient({
children,
containerClassName,
className,
as: Tag = "button",
as: Tag = 'button',
duration = 1,
clockwise = true,
...props
}: React.PropsWithChildren<
{
as?: React.ElementType;
containerClassName?: string;
className?: string;
duration?: number;
clockwise?: boolean;
as?: React.ElementType
containerClassName?: string
className?: string
duration?: number
clockwise?: boolean
} & React.HTMLAttributes<HTMLElement>
>) {
const [hovered, setHovered] = useState<boolean>(false);
const [direction, setDirection] = useState<Direction>("TOP");
const [hovered, setHovered] = useState<boolean>(false)
const [direction, setDirection] = useState<Direction>('TOP')
const rotateDirection = (currentDirection: Direction): Direction => {
const directions: Direction[] = ["TOP", "LEFT", "BOTTOM", "RIGHT"];
const currentIndex = directions.indexOf(currentDirection);
const directions: Direction[] = ['TOP', 'LEFT', 'BOTTOM', 'RIGHT']
const currentIndex = directions.indexOf(currentDirection)
const nextIndex = clockwise
? (currentIndex - 1 + directions.length) % directions.length
: (currentIndex + 1) % directions.length;
return directions[nextIndex];
};
: (currentIndex + 1) % directions.length
return directions[nextIndex]
}
const movingMap: Record<Direction, string> = {
TOP: "radial-gradient(20.7% 50% at 50% 0%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
LEFT: "radial-gradient(16.6% 43.1% at 0% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
BOTTOM:
"radial-gradient(20.7% 50% at 50% 100%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
RIGHT:
"radial-gradient(16.2% 41.199999999999996% at 100% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
};
TOP: 'radial-gradient(20.7% 50% at 50% 0%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)',
LEFT: 'radial-gradient(16.6% 43.1% at 0% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)',
BOTTOM: 'radial-gradient(20.7% 50% at 50% 100%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)',
RIGHT: 'radial-gradient(16.2% 41.199999999999996% at 100% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)',
}
const highlight =
"radial-gradient(75% 181.15942028985506% at 50% 50%, #3275F8 0%, rgba(255, 255, 255, 0) 100%)";
'radial-gradient(75% 181.15942028985506% at 50% 50%, #3275F8 0%, rgba(255, 255, 255, 0) 100%)'
useEffect(() => {
if (!hovered) {
const interval = setInterval(() => {
setDirection((prevState) => rotateDirection(prevState));
}, duration * 1000);
return () => clearInterval(interval);
setDirection((prevState) => rotateDirection(prevState))
}, duration * 1000)
return () => clearInterval(interval)
}
}, [hovered]);
}, [hovered])
return (
<Tag
onMouseEnter={(event: React.MouseEvent<HTMLDivElement>) => {
setHovered(true);
setHovered(true)
}}
onMouseLeave={() => setHovered(false)}
className={cn(
"relative flex rounded-full border content-center bg-black/20 hover:bg-black/10 transition duration-500 dark:bg-white/20 items-center flex-col flex-nowrap gap-10 h-min justify-center overflow-visible p-px decoration-clone w-fit",
'relative flex h-min w-fit flex-col flex-nowrap content-center items-center justify-center gap-10 overflow-visible rounded-full border bg-black/20 decoration-clone p-px transition duration-500 hover:bg-black/10 dark:bg-white/20',
containerClassName,
)}
{...props}
>
<div
className={cn(
"w-auto text-white z-10 bg-black px-4 py-2 rounded-[inherit]",
'z-10 w-auto rounded-[inherit] bg-black px-4 py-2 text-white',
className,
)}
>
@@ -78,13 +76,13 @@ export function HoverBorderGradient({
</div>
<motion.div
className={cn(
"flex-none inset-0 overflow-hidden absolute z-0 rounded-[inherit]",
'absolute inset-0 z-0 flex-none overflow-hidden rounded-[inherit]',
)}
style={{
filter: "blur(2px)",
position: "absolute",
width: "100%",
height: "100%",
filter: 'blur(2px)',
position: 'absolute',
width: '100%',
height: '100%',
}}
initial={{ background: movingMap[direction] }}
animate={{
@@ -92,9 +90,9 @@ export function HoverBorderGradient({
? [movingMap[direction], highlight]
: movingMap[direction],
}}
transition={{ ease: "linear", duration: duration ?? 1 }}
transition={{ ease: 'linear', duration: duration ?? 1 }}
/>
<div className="bg-black absolute z-1 flex-none inset-[2px] rounded-[100px]" />
<div className="z-1 absolute inset-[2px] flex-none rounded-[100px] bg-black" />
</Tag>
);
)
}

View File

@@ -1,9 +1,9 @@
import { cn } from "@/lib/utils";
import * as React from "react";
import { cn } from '@/lib/utils'
import * as React from 'react'
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
errorMessage?: string;
errorMessage?: string
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
@@ -14,7 +14,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
type={type}
className={cn(
// bg-gray
"flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
'flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
ref={ref}
@@ -26,26 +26,28 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
</span>
)}
</>
);
)
},
);
Input.displayName = "Input";
)
Input.displayName = 'Input'
const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
({ className, errorMessage, ...props }, ref) => {
return (
<Input
type="text"
className={cn("text-left", className)}
className={cn('text-left', className)}
ref={ref}
{...props}
value={props.value === undefined ? undefined : String(props.value)}
value={
props.value === undefined ? undefined : String(props.value)
}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
props.onChange?.(e);
const value = e.target.value
if (value === '') {
props.onChange?.(e)
} else {
const number = Number.parseInt(value, 10);
const number = Number.parseInt(value, 10)
if (!Number.isNaN(number)) {
const syntheticEvent = {
...e,
@@ -53,17 +55,17 @@ const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
...e.target,
value: number,
},
};
}
props.onChange?.(
syntheticEvent as unknown as React.ChangeEvent<HTMLInputElement>,
);
)
}
}
}}
/>
);
)
},
);
NumberInput.displayName = "NumberInput";
)
NumberInput.displayName = 'NumberInput'
export { Input, NumberInput };
export { Input, NumberInput }

View File

@@ -1,13 +1,13 @@
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils'
interface MarqueeProps {
className?: string;
reverse?: boolean;
pauseOnHover?: boolean;
children?: React.ReactNode;
vertical?: boolean;
repeat?: number;
[key: string]: any;
className?: string
reverse?: boolean
pauseOnHover?: boolean
children?: React.ReactNode
vertical?: boolean
repeat?: number
[key: string]: any
}
export function Marquee({
@@ -23,10 +23,10 @@ export function Marquee({
<div
{...props}
className={cn(
"group flex overflow-hidden p-2 [--duration:40s] [--gap:1rem] [gap:var(--gap)]",
'group flex overflow-hidden p-2 [--duration:40s] [--gap:1rem] [gap:var(--gap)]',
{
"flex-row": !vertical,
"flex-col": vertical,
'flex-row': !vertical,
'flex-col': vertical,
},
className,
)}
@@ -36,16 +36,20 @@ export function Marquee({
.map((_, i) => (
<div
key={i}
className={cn("flex shrink-0 justify-around [gap:var(--gap)]", {
"animate-marquee flex-row": !vertical,
"animate-marquee-vertical flex-col": vertical,
"group-hover:[animation-play-state:paused]": pauseOnHover,
"[animation-direction:reverse]": reverse,
})}
className={cn(
'flex shrink-0 justify-around [gap:var(--gap)]',
{
'animate-marquee flex-row': !vertical,
'animate-marquee-vertical flex-col': vertical,
'group-hover:[animation-play-state:paused]':
pauseOnHover,
'[animation-direction:reverse]': reverse,
},
)}
>
{children}
</div>
))}
</div>
);
)
}

View File

@@ -1,58 +1,58 @@
"use client";
'use client'
import { useInView, useMotionValue, useSpring } from "framer-motion";
import { useEffect, useRef } from "react";
import { useInView, useMotionValue, useSpring } from 'framer-motion'
import { useEffect, useRef } from 'react'
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils'
export default function NumberTicker({
value,
direction = "up",
direction = 'up',
delay = 0,
className,
decimalPlaces = 0,
}: {
value: number;
direction?: "up" | "down";
className?: string;
delay?: number; // delay in s
decimalPlaces?: number;
value: number
direction?: 'up' | 'down'
className?: string
delay?: number // delay in s
decimalPlaces?: number
}) {
const ref = useRef<HTMLSpanElement>(null);
const motionValue = useMotionValue(direction === "down" ? value : 0);
const ref = useRef<HTMLSpanElement>(null)
const motionValue = useMotionValue(direction === 'down' ? value : 0)
const springValue = useSpring(motionValue, {
damping: 60,
stiffness: 100,
});
const isInView = useInView(ref, { once: true, margin: "0px" });
})
const isInView = useInView(ref, { once: true, margin: '0px' })
useEffect(() => {
isInView &&
setTimeout(() => {
motionValue.set(direction === "down" ? 0 : value);
}, delay * 1000);
}, [motionValue, isInView, delay, value, direction]);
motionValue.set(direction === 'down' ? 0 : value)
}, delay * 1000)
}, [motionValue, isInView, delay, value, direction])
useEffect(
() =>
springValue.on("change", (latest) => {
springValue.on('change', (latest) => {
if (ref.current) {
ref.current.textContent = Intl.NumberFormat("en-US", {
ref.current.textContent = Intl.NumberFormat('en-US', {
minimumFractionDigits: decimalPlaces,
maximumFractionDigits: decimalPlaces,
}).format(Number(latest.toFixed(decimalPlaces)));
}).format(Number(latest.toFixed(decimalPlaces)))
}
}),
[springValue, decimalPlaces],
);
)
return (
<span
className={cn(
"inline-block tabular-nums text-white tracking-wider",
'inline-block tabular-nums tracking-wider text-white',
className,
)}
ref={ref}
/>
);
)
}

View File

@@ -1,29 +1,29 @@
"use client";
import React from "react";
'use client'
import React from 'react'
import { cn } from "@/lib/utils";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import Link from "next/link";
import { Avatar, AvatarFallback, AvatarImage } from "./avatar";
import { cn } from '@/lib/utils'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import Link from 'next/link'
import { Avatar, AvatarFallback, AvatarImage } from './avatar'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./tooltip";
} from './tooltip'
interface RippleProps {
mainCircleSize?: number;
mainCircleOpacity?: number;
numCircles?: number;
className?: string;
mainCircleSize?: number
mainCircleOpacity?: number
numCircles?: number
className?: string
}
type AvatarItem = {
name: string;
image: string;
link: string;
type: "hero" | "premium" | "elite" | "supporting" | "community";
};
name: string
image: string
link: string
type: 'hero' | 'premium' | 'elite' | 'supporting' | 'community'
}
const Ripple = React.memo(function Ripple({
mainCircleSize = 210,
@@ -33,87 +33,83 @@ const Ripple = React.memo(function Ripple({
}: RippleProps) {
const heroSponsors: AvatarItem[] = [
{
name: "Hostinger",
image: "https://avatars.githubusercontent.com/u/2630767?s=200&v=4",
link: "https://www.hostinger.com/vps-hosting?ref=dokploy",
type: "hero",
name: 'Hostinger',
image: 'https://avatars.githubusercontent.com/u/2630767?s=200&v=4',
link: 'https://www.hostinger.com/vps-hosting?ref=dokploy',
type: 'hero',
},
{
name: "Lxaer",
image:
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/.github/sponsors/lxaer.png",
link: "https://www.lxaer.com?ref=dokploy",
type: "hero",
name: 'Lxaer',
image: 'https://raw.githubusercontent.com/Dokploy/dokploy/canary/.github/sponsors/lxaer.png',
link: 'https://www.lxaer.com?ref=dokploy',
type: 'hero',
},
{
name: "LambdaTest",
image: "https://www.lambdatest.com/blue-logo.png",
link: "https://www.lambdatest.com/?utm_source=dokploy&utm_medium=sponsor",
type: "premium",
name: 'LambdaTest',
image: 'https://www.lambdatest.com/blue-logo.png',
link: 'https://www.lambdatest.com/?utm_source=dokploy&utm_medium=sponsor',
type: 'premium',
},
];
]
const premiumSponsors = [
{
name: "Supafort",
image: "supafort.png",
link: "https://supafort.com/?ref=dokploy",
type: "premium",
name: 'Supafort',
image: 'supafort.png',
link: 'https://supafort.com/?ref=dokploy',
type: 'premium',
},
{
name: "AgentDock",
image:
"https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/.github/sponsors/agentdock.png",
link: "https://agentdock.ai/?ref=dokploy",
type: "premium",
name: 'AgentDock',
image: 'https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/.github/sponsors/agentdock.png',
link: 'https://agentdock.ai/?ref=dokploy',
type: 'premium',
},
];
]
const eliteSponsors = [
{
name: "AmericanCloud",
image:
"https://media.licdn.com/dms/image/v2/D560BAQGQ0rVfEgLUMQ/company-logo_200_200/company-logo_200_200/0/1722459194382/americancloud_logo?e=2147483647&v=beta&t=990H-OldnorPQbgbN3jHihJijOb2aDmhwFl8DU_d680",
link: "https://americancloud.com/?ref=dokploy",
type: "elite",
name: 'AmericanCloud',
image: 'https://media.licdn.com/dms/image/v2/D560BAQGQ0rVfEgLUMQ/company-logo_200_200/company-logo_200_200/0/1722459194382/americancloud_logo?e=2147483647&v=beta&t=990H-OldnorPQbgbN3jHihJijOb2aDmhwFl8DU_d680',
link: 'https://americancloud.com/?ref=dokploy',
type: 'elite',
},
{
name: "Tolgee",
image: "tolgee-logo.png",
link: "https://tolg.ee/hrszh9",
type: "elite",
name: 'Tolgee',
image: 'tolgee-logo.png',
link: 'https://tolg.ee/hrszh9',
type: 'elite',
},
];
]
const supportingSponsors = [
{
name: "Cloudblast",
image: "https://cloudblast.io/img/logo-icon.193cf13e.svg",
link: "https://cloudblast.io/?ref=dokploy",
type: "supporting",
name: 'Cloudblast',
image: 'https://cloudblast.io/img/logo-icon.193cf13e.svg',
link: 'https://cloudblast.io/?ref=dokploy',
type: 'supporting',
},
{
name: "Synexa",
image:
"https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/.github/sponsors/synexa.png",
link: "https://synexa.ai/?ref=dokploy",
type: "supporting",
name: 'Synexa',
image: 'https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/.github/sponsors/synexa.png',
link: 'https://synexa.ai/?ref=dokploy',
type: 'supporting',
},
{
name: "HahuCloud",
image: "hahucloud_logo_1.png",
link: "https://www.hahucloud.com/?ref=dokploy",
type: "supporting",
name: 'HahuCloud',
image: 'hahucloud_logo_1.png',
link: 'https://www.hahucloud.com/?ref=dokploy',
type: 'supporting',
},
{
name: "Teramont",
image: "terramont.ico",
link: "https://teramont.net/dokploy",
type: "supporting",
name: 'Teramont',
image: 'terramont.ico',
link: 'https://teramont.net/dokploy',
type: 'supporting',
},
];
]
const communitySponsors = [];
const communitySponsors = []
return (
<div
@@ -124,84 +120,104 @@ const Ripple = React.memo(function Ripple({
>
<div>
{Array.from({ length: numCircles }, (_, i) => {
const size = mainCircleSize + i * 70;
const opacity = mainCircleOpacity - i * 0.03;
const animationDelay = `${i * 0.06}s`;
const borderStyle = i === numCircles - 1 ? "dashed" : "solid";
const borderOpacity = 5 + i * 5;
const size = mainCircleSize + i * 70
const opacity = mainCircleOpacity - i * 0.03
const animationDelay = `${i * 0.06}s`
const borderStyle =
i === numCircles - 1 ? 'dashed' : 'solid'
const borderOpacity = 5 + i * 5
return (
<div
key={i}
className={`absolute animate-ripple rounded-full bg-foreground/25 shadow-xl border [--i:${i}]`}
className={`absolute animate-ripple rounded-full border bg-foreground/25 shadow-xl [--i:${i}]`}
style={{
width: `${size}px`,
height: `${size}px`,
opacity,
animationDelay,
borderStyle,
borderWidth: "1px",
borderWidth: '1px',
borderColor: `hsl(var(--foreground), ${borderOpacity / 100})`,
top: "50%",
left: "50%",
transform: "translate(-50%, -50%) scale(1)",
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%) scale(1)',
}}
/>
);
)
})}
{Array.from({ length: numCircles }, (_, i) => {
const size = mainCircleSize + i * 70;
const opacity = mainCircleOpacity - i * 0.03;
const animationDelay = `${i * 0.06}s`;
const borderStyle = i === numCircles - 1 ? "dashed" : "solid";
const borderOpacity = 5 + i * 5;
const size = mainCircleSize + i * 70
const opacity = mainCircleOpacity - i * 0.03
const animationDelay = `${i * 0.06}s`
const borderStyle =
i === numCircles - 1 ? 'dashed' : 'solid'
const borderOpacity = 5 + i * 5
return (
<div
key={i}
className={`absolute z-30 animate-ripple rounded-full shadow-xl border [--i:${i}]`}
className={`absolute z-30 animate-ripple rounded-full border shadow-xl [--i:${i}]`}
style={{
animationDelay,
borderStyle,
borderWidth: "1px",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%) scale(1)",
borderWidth: '1px',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%) scale(1)',
}}
>
{i === 0 && (
<div className="relative w-full h-full flex justify-center items-center">
<div className="relative flex h-full w-full items-center justify-center">
{heroSponsors.map((item, index) => {
const angle = (360 / heroSponsors.length) * index;
const radius = mainCircleSize / 2;
const x = radius * Math.cos((angle * Math.PI) / 180);
const y = radius * Math.sin((angle * Math.PI) / 180);
const angle =
(360 / heroSponsors.length) * index
const radius = mainCircleSize / 2
const x =
radius *
Math.cos((angle * Math.PI) / 180)
const y =
radius *
Math.sin((angle * Math.PI) / 180)
const initials = item.name
.split(" ")
.split(' ')
.map((n) => n[0])
.join("");
.join('')
return (
<div
key={index}
className="absolute"
style={{
top: "50%",
left: "50%",
top: '50%',
left: '50%',
transform: `translate(${x}px, ${y}px) translate(-50%, -50%)`,
}}
>
<TooltipProvider delayDuration={100}>
<TooltipProvider
delayDuration={100}
>
<Tooltip>
<TooltipTrigger>
<Link href={item.link} target="_blank">
<Link
href={item.link}
target="_blank"
>
<Avatar className="border-2 border-red-600">
<AvatarImage
src={item.image}
alt={item.name}
src={
item.image
}
alt={
item.name
}
className="object-contain"
/>
<AvatarFallback>{initials}</AvatarFallback>
<AvatarFallback>
{
initials
}
</AvatarFallback>
</Avatar>
</Link>
</TooltipTrigger>
@@ -215,42 +231,61 @@ const Ripple = React.memo(function Ripple({
</Tooltip>
</TooltipProvider>
</div>
);
)
})}
</div>
)}
{i === 1 && (
<div className="relative w-full h-full flex justify-center items-center">
<div className="relative flex h-full w-full items-center justify-center">
{premiumSponsors.map((item, index) => {
const angle = (360 / premiumSponsors.length) * index;
const radius = mainCircleSize / 2 + 70;
const x = radius * Math.cos((angle * Math.PI) / 180);
const y = radius * Math.sin((angle * Math.PI) / 180);
const angle =
(360 / premiumSponsors.length) *
index
const radius = mainCircleSize / 2 + 70
const x =
radius *
Math.cos((angle * Math.PI) / 180)
const y =
radius *
Math.sin((angle * Math.PI) / 180)
const initials = item.name
.split(" ")
.split(' ')
.map((n) => n[0])
.join("");
.join('')
return (
<div
key={index}
className="absolute"
style={{
top: "50%",
left: "50%",
top: '50%',
left: '50%',
transform: `translate(${x}px, ${y}px) translate(-50%, -50%)`,
}}
>
<TooltipProvider delayDuration={100}>
<TooltipProvider
delayDuration={100}
>
<Tooltip>
<TooltipTrigger>
<Link href={item.link} target="_blank">
<Link
href={item.link}
target="_blank"
>
<Avatar className="border-2 border-yellow-500">
<AvatarImage
src={item.image}
alt={item.name}
src={
item.image
}
alt={
item.name
}
/>
<AvatarFallback>{initials}</AvatarFallback>
<AvatarFallback>
{
initials
}
</AvatarFallback>
</Avatar>
</Link>
</TooltipTrigger>
@@ -264,42 +299,60 @@ const Ripple = React.memo(function Ripple({
</Tooltip>
</TooltipProvider>
</div>
);
)
})}
</div>
)}
{i === 2 && (
<div className="relative w-full h-full flex justify-center items-center">
<div className="relative flex h-full w-full items-center justify-center">
{eliteSponsors.map((item, index) => {
const angle = (360 / eliteSponsors.length) * index;
const radius = mainCircleSize / 2 + 100;
const x = radius * Math.cos((angle * Math.PI) / 180);
const y = radius * Math.sin((angle * Math.PI) / 180);
const angle =
(360 / eliteSponsors.length) * index
const radius = mainCircleSize / 2 + 100
const x =
radius *
Math.cos((angle * Math.PI) / 180)
const y =
radius *
Math.sin((angle * Math.PI) / 180)
const initials = item.name
.split(" ")
.split(' ')
.map((n) => n[0])
.join("");
.join('')
return (
<div
key={index}
className="absolute"
style={{
top: "50%",
left: "50%",
top: '50%',
left: '50%',
transform: `translate(${x}px, ${y}px) translate(-50%, -50%)`,
}}
>
<TooltipProvider delayDuration={100}>
<TooltipProvider
delayDuration={100}
>
<Tooltip>
<TooltipTrigger>
<Link href={item.link} target="_blank">
<Link
href={item.link}
target="_blank"
>
<Avatar className="border-2 border-yellow-900">
<AvatarImage
src={item.image}
alt={item.name}
src={
item.image
}
alt={
item.name
}
/>
<AvatarFallback>{initials}</AvatarFallback>
<AvatarFallback>
{
initials
}
</AvatarFallback>
</Avatar>
</Link>
</TooltipTrigger>
@@ -313,41 +366,60 @@ const Ripple = React.memo(function Ripple({
</Tooltip>
</TooltipProvider>
</div>
);
)
})}
</div>
)}
{i === 3 && (
<div className="relative w-full h-full flex justify-center items-center">
<div className="relative flex h-full w-full items-center justify-center">
{supportingSponsors.map((item, index) => {
const angle = (360 / supportingSponsors.length) * index;
const radius = mainCircleSize / 2 + 140;
const x = radius * Math.cos((angle * Math.PI) / 180);
const y = radius * Math.sin((angle * Math.PI) / 180);
const angle =
(360 / supportingSponsors.length) *
index
const radius = mainCircleSize / 2 + 140
const x =
radius *
Math.cos((angle * Math.PI) / 180)
const y =
radius *
Math.sin((angle * Math.PI) / 180)
const initials = item.name
.split(" ")
.split(' ')
.map((n) => n[0])
.join("");
.join('')
return (
<div
key={index}
className="absolute"
style={{
top: "50%",
left: "50%",
top: '50%',
left: '50%',
transform: `translate(${x}px, ${y}px) translate(-50%, -50%)`,
}}
>
<TooltipProvider delayDuration={100}>
<TooltipProvider
delayDuration={100}
>
<Tooltip>
<TooltipTrigger>
<Link href={item.link} target="_blank">
<Link
href={item.link}
target="_blank"
>
<Avatar className="border-2 border-yellow-900">
<AvatarImage
src={item.image}
alt={item.name}
src={
item.image
}
alt={
item.name
}
/>
<AvatarFallback>{initials}</AvatarFallback>
<AvatarFallback>
{
initials
}
</AvatarFallback>
</Avatar>
</Link>
</TooltipTrigger>
@@ -361,42 +433,61 @@ const Ripple = React.memo(function Ripple({
</Tooltip>
</TooltipProvider>
</div>
);
)
})}
</div>
)}
{i === 4 && (
<div className="relative w-full h-full flex justify-center items-center">
<div className="relative flex h-full w-full items-center justify-center">
{communitySponsors.map((item, index) => {
const angle = (360 / communitySponsors.length) * index;
const radius = mainCircleSize / 2 + 180;
const x = radius * Math.cos((angle * Math.PI) / 180);
const y = radius * Math.sin((angle * Math.PI) / 180);
const angle =
(360 / communitySponsors.length) *
index
const radius = mainCircleSize / 2 + 180
const x =
radius *
Math.cos((angle * Math.PI) / 180)
const y =
radius *
Math.sin((angle * Math.PI) / 180)
const initials = item.name
.split(" ")
.split(' ')
.map((n) => n[0])
.join("");
.join('')
return (
<div
key={index}
className="absolute"
style={{
top: "50%",
left: "50%",
top: '50%',
left: '50%',
transform: `translate(${x}px, ${y}px) translate(-50%, -50%)`,
}}
>
<TooltipProvider delayDuration={100}>
<TooltipProvider
delayDuration={100}
>
<Tooltip>
<TooltipTrigger>
<Link href={item.link} target="_blank">
<Link
href={item.link}
target="_blank"
>
<Avatar className="border-2 border-yellow-500">
<AvatarImage
src={item.image}
alt={item.name}
src={
item.image
}
alt={
item.name
}
/>
<AvatarFallback>{initials}</AvatarFallback>
<AvatarFallback>
{
initials
}
</AvatarFallback>
</Avatar>
</Link>
</TooltipTrigger>
@@ -408,18 +499,18 @@ const Ripple = React.memo(function Ripple({
</Tooltip>
</TooltipProvider>
</div>
);
)
})}
</div>
)}
</div>
);
)
})}
</div>
</div>
);
});
)
})
Ripple.displayName = "Ripple";
Ripple.displayName = 'Ripple'
export default Ripple;
export default Ripple

View File

@@ -1,10 +1,10 @@
import type { SVGProps } from "react";
import type { SVGProps } from 'react'
export interface SafariProps extends SVGProps<SVGSVGElement> {
url?: string;
src?: string;
width?: number;
height?: number;
url?: string
src?: string
width?: number
height?: number
}
export default function Safari({
@@ -134,5 +134,5 @@ export default function Safari({
</clipPath>
</defs>
</svg>
);
)
}

View File

@@ -1,9 +1,9 @@
"use client";
'use client'
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import * as React from "react";
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
import * as React from 'react'
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils'
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
@@ -11,7 +11,7 @@ const ScrollArea = React.forwardRef<
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
className={cn('relative overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
@@ -20,29 +20,31 @@ const ScrollArea = React.forwardRef<
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
React.ComponentPropsWithoutRef<
typeof ScrollAreaPrimitive.ScrollAreaScrollbar
>
>(({ className, orientation = 'vertical', ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
'flex touch-none select-none transition-colors',
orientation === 'vertical' &&
'h-full w-2.5 border-l border-l-transparent p-[1px]',
orientation === 'horizontal' &&
'h-2.5 flex-col border-t border-t-transparent p-[1px]',
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar };
export { ScrollArea, ScrollBar }

View File

@@ -1,16 +1,16 @@
"use client";
'use client'
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import * as React from "react";
import * as SelectPrimitive from '@radix-ui/react-select'
import { Check, ChevronDown, ChevronUp } from 'lucide-react'
import * as React from 'react'
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils'
const Select = SelectPrimitive.Root;
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group;
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value;
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
@@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
className,
)}
{...props}
@@ -29,8 +29,8 @@ const SelectTrigger = React.forwardRef<
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
@@ -39,15 +39,15 @@ const SelectScrollUpButton = React.forwardRef<
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
'flex cursor-default items-center justify-center py-1',
className,
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
@@ -56,28 +56,28 @@ const SelectScrollDownButton = React.forwardRef<
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
'flex cursor-default items-center justify-center py-1',
className,
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
>(({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className,
)}
position={position}
@@ -86,9 +86,9 @@ const SelectContent = React.forwardRef<
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
)}
>
{children}
@@ -96,8 +96,8 @@ const SelectContent = React.forwardRef<
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
@@ -105,11 +105,11 @@ const SelectLabel = React.forwardRef<
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
@@ -118,7 +118,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
{...props}
@@ -131,8 +131,8 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
@@ -140,11 +140,11 @@ const SelectSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
@@ -157,4 +157,4 @@ export {
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};
}

View File

@@ -1,9 +1,9 @@
"use client";
'use client'
import * as SwitchPrimitives from "@radix-ui/react-switch";
import * as React from "react";
import * as SwitchPrimitives from '@radix-ui/react-switch'
import * as React from 'react'
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils'
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
@@ -11,7 +11,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
className,
)}
{...props}
@@ -19,11 +19,11 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0',
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch };
export { Switch }

View File

@@ -1,9 +1,9 @@
import * as TabsPrimitive from "@radix-ui/react-tabs";
import * as React from "react";
import * as TabsPrimitive from '@radix-ui/react-tabs'
import * as React from 'react'
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils'
const Tabs = TabsPrimitive.Root;
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
@@ -12,13 +12,13 @@ const TabsList = React.forwardRef<
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
className,
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
@@ -27,13 +27,13 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
@@ -42,12 +42,12 @@ const TabsContent = React.forwardRef<
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent };
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -1,15 +1,15 @@
"use client";
'use client'
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import * as React from "react";
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import * as React from 'react'
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils'
const TooltipProvider = TooltipPrimitive.Provider;
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root;
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
@@ -19,12 +19,12 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@@ -1,11 +1,11 @@
import GhostContentAPI from "@tryghost/content-api";
import GhostContentAPI from '@tryghost/content-api'
// Ghost API configuration
const ghostConfig = {
url: process.env.GHOST_URL || "https://site.com",
key: process.env.GHOST_KEY || "42424242424242424242424242424242",
version: "v5.0",
};
url: process.env.GHOST_URL || 'https://site.com',
key: process.env.GHOST_KEY || '42424242424242424242424242424242',
version: 'v5.0',
}
// Initialize the Ghost API with your credentials
const api = GhostContentAPI({
@@ -14,80 +14,80 @@ const api = GhostContentAPI({
version: ghostConfig.version,
// @ts-ignore
makeRequest: ({ url, method, params, headers }) => {
const apiUrl = new URL(url);
const apiUrl = new URL(url)
// @ts-ignore
Object.keys(params).map((key) =>
apiUrl.searchParams.set(key, encodeURIComponent(params[key])),
);
)
return fetch(apiUrl.toString(), { method, headers })
.then(async (res) => {
// Check if the response was successful.
if (!res.ok) {
// You can handle HTTP errors here
throw new Error(`HTTP error! status: ${res.status}`);
throw new Error(`HTTP error! status: ${res.status}`)
}
return { data: await res.json() };
return { data: await res.json() }
})
.catch((error) => {
console.error("Fetch error:", error);
});
console.error('Fetch error:', error)
})
},
});
})
export interface Post {
id: string;
uuid: string;
title: string;
slug: string;
html: string;
feature_image: string | null;
featured: boolean;
visibility: string;
created_at: string;
updated_at: string;
published_at: string;
custom_excerpt: string | null;
excerpt: string;
reading_time: number;
id: string
uuid: string
title: string
slug: string
html: string
feature_image: string | null
featured: boolean
visibility: string
created_at: string
updated_at: string
published_at: string
custom_excerpt: string | null
excerpt: string
reading_time: number
primary_tag?: {
id: string;
name: string;
slug: string;
};
id: string
name: string
slug: string
}
tags?: Array<{
id: string;
name: string;
slug: string;
}>;
id: string
name: string
slug: string
}>
primary_author?: {
id: string;
name: string;
slug: string;
profile_image: string | null;
bio: string | null;
twitter: string | null;
};
id: string
name: string
slug: string
profile_image: string | null
bio: string | null
twitter: string | null
}
authors?: Array<{
id: string;
name: string;
slug: string;
profile_image: string | null;
bio: string | null;
}>;
url: string;
id: string
name: string
slug: string
profile_image: string | null
bio: string | null
}>
url: string
}
export async function getPosts(options = {}): Promise<Post[]> {
try {
const result = (await api.posts.browse({
include: "authors",
limit: "all",
})) as Post[];
return result;
include: 'authors',
limit: 'all',
})) as Post[]
return result
} catch (error) {
console.error("Error fetching posts:", error);
return [];
console.error('Error fetching posts:', error)
return []
}
}
@@ -95,36 +95,36 @@ export async function getPost(slug: string): Promise<Post | null> {
try {
const result = (await api.posts.read({
slug,
include: ["authors"],
})) as Post;
include: ['authors'],
})) as Post
return result;
return result
} catch (error) {
console.error("Error fetching post:", error);
return null;
console.error('Error fetching post:', error)
return null
}
}
export async function getTags() {
try {
const result = await api.tags.browse();
return result;
const result = await api.tags.browse()
return result
} catch (error) {
console.error("Error fetching tags:", error);
return [];
console.error('Error fetching tags:', error)
return []
}
}
export async function getPostsByTag(tag: string) {
try {
const result = await api.posts.browse({
limit: "all",
limit: 'all',
filter: `tag:${tag}`,
include: ["tags", "authors"],
});
return result;
include: ['tags', 'authors'],
})
return result
} catch (error) {
console.error(`Error fetching posts with tag ${tag}:`, error);
return [];
console.error(`Error fetching posts with tag ${tag}:`, error)
return []
}
}

View File

@@ -1,28 +1,28 @@
import { useEffect, useRef } from "react";
import { useEffect, useRef } from 'react'
export function useDebounce<T extends (...args: any[]) => any>(
callback: T,
delay: number,
): T {
const timeoutRef = useRef<NodeJS.Timeout>();
const timeoutRef = useRef<NodeJS.Timeout>()
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
clearTimeout(timeoutRef.current)
}
};
}, []);
}
}, [])
return ((...args: Parameters<T>) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
clearTimeout(timeoutRef.current)
}
return new Promise<ReturnType<T>>((resolve) => {
timeoutRef.current = setTimeout(() => {
resolve(callback(...args));
}, delay);
});
}) as T;
resolve(callback(...args))
}, delay)
})
}) as T
}

View File

@@ -1,25 +1,25 @@
interface HubSpotFormField {
objectTypeId: string;
name: string;
value: string;
objectTypeId: string
name: string
value: string
}
interface HubSpotFormData {
fields: HubSpotFormField[];
fields: HubSpotFormField[]
context: {
pageUri: string;
pageName: string;
hutk?: string; // HubSpot UTK from cookies
};
pageUri: string
pageName: string
hutk?: string // HubSpot UTK from cookies
}
}
interface ContactFormData {
inquiryType: "support" | "sales" | "other";
firstName: string;
lastName: string;
email: string;
company: string;
message: string;
inquiryType: 'support' | 'sales' | 'other'
firstName: string
lastName: string
email: string
company: string
message: string
}
/**
@@ -27,19 +27,19 @@ interface ContactFormData {
* This is used for tracking and attribution in HubSpot
*/
export function getHubSpotUTK(cookieHeader?: string): string | null {
if (!cookieHeader) return null;
if (!cookieHeader) return null
const name = "hubspotutk=";
const decodedCookie = decodeURIComponent(cookieHeader);
const cookieArray = decodedCookie.split(";");
const name = 'hubspotutk='
const decodedCookie = decodeURIComponent(cookieHeader)
const cookieArray = decodedCookie.split(';')
for (let i = 0; i < cookieArray.length; i++) {
const cookie = cookieArray[i].trim();
const cookie = cookieArray[i].trim()
if (cookie.indexOf(name) === 0) {
return cookie.substring(name.length, cookie.length);
return cookie.substring(name.length, cookie.length)
}
}
return null;
return null
}
/**
@@ -52,43 +52,43 @@ export function formatContactDataForHubSpot(
const formData: HubSpotFormData = {
fields: [
{
objectTypeId: "0-1", // Contact object type
name: "firstname",
objectTypeId: '0-1', // Contact object type
name: 'firstname',
value: contactData.firstName,
},
{
objectTypeId: "0-1",
name: "lastname",
objectTypeId: '0-1',
name: 'lastname',
value: contactData.lastName,
},
{
objectTypeId: "0-1",
name: "email",
objectTypeId: '0-1',
name: 'email',
value: contactData.email,
},
{
objectTypeId: "0-1",
name: "message",
objectTypeId: '0-1',
name: 'message',
value: contactData.message,
},
{
objectTypeId: "0-2", // Company object type
name: "name",
objectTypeId: '0-2', // Company object type
name: 'name',
value: contactData.company,
},
],
context: {
pageUri: "https://dokploy.com/contact",
pageName: "Contact Us",
pageUri: 'https://dokploy.com/contact',
pageName: 'Contact Us',
},
};
}
// Add HubSpot UTK if available
if (hutk) {
formData.context.hutk = hutk;
formData.context.hutk = hutk
}
return formData;
return formData
}
/**
@@ -99,40 +99,40 @@ export async function submitToHubSpot(
hutk?: string | null,
): Promise<boolean> {
try {
const portalId = process.env.HUBSPOT_PORTAL_ID;
const formGuid = process.env.HUBSPOT_FORM_GUID;
const portalId = process.env.HUBSPOT_PORTAL_ID
const formGuid = process.env.HUBSPOT_FORM_GUID
if (!portalId || !formGuid) {
console.error(
"HubSpot configuration missing: HUBSPOT_PORTAL_ID or HUBSPOT_FORM_GUID not set",
);
return false;
'HubSpot configuration missing: HUBSPOT_PORTAL_ID or HUBSPOT_FORM_GUID not set',
)
return false
}
const formData = formatContactDataForHubSpot(contactData, hutk);
const formData = formatContactDataForHubSpot(contactData, hutk)
const response = await fetch(
`https://api.hsforms.com/submissions/v3/integration/submit/${portalId}/${formGuid}`,
{
method: "POST",
method: 'POST',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
},
);
)
if (!response.ok) {
const errorText = await response.text();
console.error("HubSpot API error:", response.status, errorText);
return false;
const errorText = await response.text()
console.error('HubSpot API error:', response.status, errorText)
return false
}
const result = await response.json();
console.log("HubSpot submission successful:", result);
return true;
const result = await response.json()
console.log('HubSpot submission successful:', result)
return true
} catch (error) {
console.error("Error submitting to HubSpot:", error);
return false;
console.error('Error submitting to HubSpot:', error)
return false
}
}

View File

@@ -1,48 +1,48 @@
import * as fs from "node:fs/promises";
import { join } from "node:path";
import satori from "satori";
import sharp from "sharp";
import * as fs from 'node:fs/promises'
import { join } from 'node:path'
import satori from 'satori'
import sharp from 'sharp'
interface GenerateOGImageOptions {
title: string;
title: string
author?: {
name: string;
image?: string;
};
date?: string;
readingTime?: number;
name: string
image?: string
}
date?: string
readingTime?: number
}
// Logo de Dokploy como SVG string (versión simplificada)
const DOKPLOY_LOGO = {
type: "svg",
type: 'svg',
props: {
width: "100%",
height: "100%",
viewBox: "0 0 6323 5778",
fill: "currentColor",
width: '100%',
height: '100%',
viewBox: '0 0 6323 5778',
fill: 'currentColor',
children: [
{
type: "path",
type: 'path',
props: {
d: "M4638.51 44.5295C4616.52 81.8286 4611.45 115.575 4619.9 213.263C4636.82 433.505 4772.12 710.584 4924.33 842.019C5002.12 909.512 5196.61 1012.53 5245.66 1012.53C5284.56 1012.53 5282.87 1019.63 5213.53 1129.75C5140.8 1243.43 5024.11 1339.34 4890.5 1389.07C4743.36 1445.91 4455.85 1453.01 4234.3 1405.06C4016.13 1357.1 3931.57 1323.35 3211.11 977.006C2265.71 522.312 2253.87 516.984 2125.34 481.461C2017.1 451.267 1917.32 445.938 1316.93 435.281C853.533 428.177 601.539 429.953 538.964 444.162C334.325 485.013 156.745 632.434 70.4925 829.586C12.9907 961.021 -7.30411 1191.92 2.84328 1589.78C7.91697 1841.99 16.3731 1911.26 46.8153 2005.39C114.465 2213.2 226.086 2342.86 422.269 2445.88C1594.29 3055.1 1969.74 3206.07 2529.54 3294.88C2732.49 3326.85 3258.46 3330.4 3459.72 3303.76C3755.69 3261.13 4107.46 3161.66 4403.43 3033.78C4540.42 2975.17 4904.03 2776.24 5220.29 2587.97C5910.31 2177.68 6006.71 2111.96 6037.16 2030.26C6070.98 1934.35 5988.11 1811.79 5888.33 1811.79C5851.12 1811.79 5862.96 1806.47 5426.62 2069.34C4352.69 2715.85 4026.28 2865.05 3485.09 2957.41C3162.06 3014.24 2587.04 2987.6 2274.17 2902.35C1924.08 2806.44 1839.52 2770.91 1051.41 2383.71C552.493 2140.38 444.255 2079.99 395.209 2023.16C363.076 1984.08 336.016 1945.01 336.016 1934.35C336.016 1920.14 467.932 1916.59 787.575 1921.92L1240.82 1929.02L1435.32 2001.84C1541.86 2040.92 1744.81 2126.17 1883.49 2190.11C2296.15 2381.94 2610.72 2451.21 3058.9 2451.21C3490.16 2451.21 3872.38 2374.83 4305.33 2198.99C4910.8 1955.66 5342.06 1596.88 5545.01 1172.38C5565.3 1127.98 5585.6 1090.68 5587.29 1087.13C5590.67 1083.57 5660.01 1074.69 5742.88 1065.81C5940.76 1046.28 6084.51 978.782 6221.5 842.019L6322.97 740.779V520.536V302.071L6253.63 353.579C6177.53 412.192 6062.52 444.162 5920.46 444.162C5795.31 444.162 5661.7 508.104 5568.69 614.672L5497.65 692.823L5487.51 646.643C5451.99 500.999 5304.85 364.236 5115.44 300.294C4956.46 248.786 4893.88 206.159 4831.31 108.471C4800.87 64.0671 4770.42 21.4395 4760.28 14.335C4721.38 -14.0833 4665.57 1.90186 4638.51 44.5295ZM2057.69 806.496C2162.55 834.914 2250.49 873.99 2517.7 1007.2C2605.65 1051.6 2796.76 1142.19 2940.51 1211.46C3084.27 1280.73 3332.88 1397.95 3490.16 1472.55C3948.49 1691.02 4049.96 1726.54 4301.95 1754.96L4437.25 1770.94L4310.41 1833.11C4153.12 1911.26 4016.13 1960.99 3804.73 2016.05C3512.15 2090.65 3402.22 2104.86 3050.44 2104.86C2590.43 2103.08 2370.57 2056.9 1974.82 1872.18C1413.33 1611.09 1386.27 1603.99 801.104 1589.78C457.784 1580.9 356.311 1572.01 336.016 1552.48C278.514 1492.09 303.882 1019.63 373.223 914.841C412.121 854.452 474.697 806.496 552.493 779.854C577.862 770.973 904.27 767.421 1278.03 772.749C1814.15 778.078 1978.2 785.182 2057.69 806.496Z",
d: 'M4638.51 44.5295C4616.52 81.8286 4611.45 115.575 4619.9 213.263C4636.82 433.505 4772.12 710.584 4924.33 842.019C5002.12 909.512 5196.61 1012.53 5245.66 1012.53C5284.56 1012.53 5282.87 1019.63 5213.53 1129.75C5140.8 1243.43 5024.11 1339.34 4890.5 1389.07C4743.36 1445.91 4455.85 1453.01 4234.3 1405.06C4016.13 1357.1 3931.57 1323.35 3211.11 977.006C2265.71 522.312 2253.87 516.984 2125.34 481.461C2017.1 451.267 1917.32 445.938 1316.93 435.281C853.533 428.177 601.539 429.953 538.964 444.162C334.325 485.013 156.745 632.434 70.4925 829.586C12.9907 961.021 -7.30411 1191.92 2.84328 1589.78C7.91697 1841.99 16.3731 1911.26 46.8153 2005.39C114.465 2213.2 226.086 2342.86 422.269 2445.88C1594.29 3055.1 1969.74 3206.07 2529.54 3294.88C2732.49 3326.85 3258.46 3330.4 3459.72 3303.76C3755.69 3261.13 4107.46 3161.66 4403.43 3033.78C4540.42 2975.17 4904.03 2776.24 5220.29 2587.97C5910.31 2177.68 6006.71 2111.96 6037.16 2030.26C6070.98 1934.35 5988.11 1811.79 5888.33 1811.79C5851.12 1811.79 5862.96 1806.47 5426.62 2069.34C4352.69 2715.85 4026.28 2865.05 3485.09 2957.41C3162.06 3014.24 2587.04 2987.6 2274.17 2902.35C1924.08 2806.44 1839.52 2770.91 1051.41 2383.71C552.493 2140.38 444.255 2079.99 395.209 2023.16C363.076 1984.08 336.016 1945.01 336.016 1934.35C336.016 1920.14 467.932 1916.59 787.575 1921.92L1240.82 1929.02L1435.32 2001.84C1541.86 2040.92 1744.81 2126.17 1883.49 2190.11C2296.15 2381.94 2610.72 2451.21 3058.9 2451.21C3490.16 2451.21 3872.38 2374.83 4305.33 2198.99C4910.8 1955.66 5342.06 1596.88 5545.01 1172.38C5565.3 1127.98 5585.6 1090.68 5587.29 1087.13C5590.67 1083.57 5660.01 1074.69 5742.88 1065.81C5940.76 1046.28 6084.51 978.782 6221.5 842.019L6322.97 740.779V520.536V302.071L6253.63 353.579C6177.53 412.192 6062.52 444.162 5920.46 444.162C5795.31 444.162 5661.7 508.104 5568.69 614.672L5497.65 692.823L5487.51 646.643C5451.99 500.999 5304.85 364.236 5115.44 300.294C4956.46 248.786 4893.88 206.159 4831.31 108.471C4800.87 64.0671 4770.42 21.4395 4760.28 14.335C4721.38 -14.0833 4665.57 1.90186 4638.51 44.5295ZM2057.69 806.496C2162.55 834.914 2250.49 873.99 2517.7 1007.2C2605.65 1051.6 2796.76 1142.19 2940.51 1211.46C3084.27 1280.73 3332.88 1397.95 3490.16 1472.55C3948.49 1691.02 4049.96 1726.54 4301.95 1754.96L4437.25 1770.94L4310.41 1833.11C4153.12 1911.26 4016.13 1960.99 3804.73 2016.05C3512.15 2090.65 3402.22 2104.86 3050.44 2104.86C2590.43 2103.08 2370.57 2056.9 1974.82 1872.18C1413.33 1611.09 1386.27 1603.99 801.104 1589.78C457.784 1580.9 356.311 1572.01 336.016 1552.48C278.514 1492.09 303.882 1019.63 373.223 914.841C412.121 854.452 474.697 806.496 552.493 779.854C577.862 770.973 904.27 767.421 1278.03 772.749C1814.15 778.078 1978.2 785.182 2057.69 806.496Z',
},
},
{
type: "path",
type: 'path',
props: {
d: "M1266.2 1060.49C1173.18 1097.79 1129.21 1207.91 1171.49 1294.94C1222.22 1394.4 1332.15 1417.49 1413.33 1342.89C1477.6 1286.06 1479.29 1174.16 1418.41 1112C1374.44 1065.82 1308.48 1042.73 1266.2 1060.49Z",
d: 'M1266.2 1060.49C1173.18 1097.79 1129.21 1207.91 1171.49 1294.94C1222.22 1394.4 1332.15 1417.49 1413.33 1342.89C1477.6 1286.06 1479.29 1174.16 1418.41 1112C1374.44 1065.82 1308.48 1042.73 1266.2 1060.49Z',
},
},
{
type: "path",
type: 'path',
props: {
d: "M87.4063 2513.37C7.91846 2548.89 -8.99385 2616.39 4.536 2836.63C19.7571 3072.86 46.8168 3222.05 124.613 3488.48C427.344 4532.85 1129.2 5287.71 2106.74 5623.4C2641.17 5806.35 3236.48 5827.66 3752.3 5682.01C4596.23 5445.79 5315 4836.57 5692.15 4040.86C5886.64 3630.57 6018.55 3111.93 6018.55 2753.15C6018.55 2582.64 5991.49 2518.7 5910.31 2497.39C5820.68 2474.3 5575.45 2609.28 5164.48 2911.23C4484.61 3410.32 4229.23 3563.07 3890.98 3676.75C3635.61 3763.78 3466.49 3797.52 3194.2 3818.84C2651.31 3863.24 2057.69 3731.81 1570.62 3458.28C1394.73 3358.82 846.769 2980.5 581.246 2772.69C285.28 2540.01 270.059 2529.36 199.028 2508.04C155.056 2495.61 124.613 2497.39 87.4063 2513.37ZM5678.62 3076.41C5661.7 3138.57 5646.48 3202.52 5646.48 3218.5C5646.48 3236.26 5626.19 3262.9 5600.82 3280.67C5573.76 3296.65 5482.43 3371.25 5396.18 3445.85C5308.24 3518.67 5198.31 3611.03 5150.95 3650.1C5101.91 3689.18 4990.28 3781.54 4902.34 3856.14C4699.39 4026.65 4406.81 4236.23 4242.76 4330.37C4085.48 4420.95 3767.52 4532.85 3532.44 4582.58C2847.5 4724.67 2054.31 4570.15 1516.5 4190.05C1173.18 3946.72 412.123 3314.41 388.445 3254.02C363.077 3182.98 330.944 3042.66 337.708 3021.35C341.091 3012.47 417.196 3060.42 505.14 3129.69C1056.48 3559.52 1563.85 3863.24 1942.69 3992.9C2328.29 4124.34 2565.06 4163.41 2991.25 4163.41C3380.23 4163.41 3628.84 4126.11 3963.71 4012.44C4345.93 3884.56 4531.96 3781.54 5052.86 3405C5391.11 3161.66 5676.92 2968.06 5700.6 2966.29C5705.68 2966.29 5697.22 3016.02 5678.62 3076.41ZM5426.62 3881C5426.62 3886.33 5409.71 3925.41 5391.11 3966.26C5318.38 4115.45 5144.19 4364.11 5003.81 4518.64C4587.77 4973.33 4090.55 5271.73 3540.9 5392.5C3309.2 5444.01 2708.81 5440.46 2483.88 5387.17C1716.06 5204.23 1105.53 4754.87 696.249 4071.05C647.204 3987.57 609.997 3916.53 613.379 3912.97C616.762 3909.42 774.046 4028.42 965.155 4177.62C1154.57 4326.82 1371.05 4486.67 1443.77 4532.85C1974.82 4863.21 2463.59 4991.09 3118.09 4968C3461.41 4955.57 3691.42 4912.94 3997.53 4806.38C4357.76 4680.27 4623.29 4513.31 5130.66 4095.92C5382.65 3888.11 5426.62 3856.14 5426.62 3881Z",
d: 'M87.4063 2513.37C7.91846 2548.89 -8.99385 2616.39 4.536 2836.63C19.7571 3072.86 46.8168 3222.05 124.613 3488.48C427.344 4532.85 1129.2 5287.71 2106.74 5623.4C2641.17 5806.35 3236.48 5827.66 3752.3 5682.01C4596.23 5445.79 5315 4836.57 5692.15 4040.86C5886.64 3630.57 6018.55 3111.93 6018.55 2753.15C6018.55 2582.64 5991.49 2518.7 5910.31 2497.39C5820.68 2474.3 5575.45 2609.28 5164.48 2911.23C4484.61 3410.32 4229.23 3563.07 3890.98 3676.75C3635.61 3763.78 3466.49 3797.52 3194.2 3818.84C2651.31 3863.24 2057.69 3731.81 1570.62 3458.28C1394.73 3358.82 846.769 2980.5 581.246 2772.69C285.28 2540.01 270.059 2529.36 199.028 2508.04C155.056 2495.61 124.613 2497.39 87.4063 2513.37ZM5678.62 3076.41C5661.7 3138.57 5646.48 3202.52 5646.48 3218.5C5646.48 3236.26 5626.19 3262.9 5600.82 3280.67C5573.76 3296.65 5482.43 3371.25 5396.18 3445.85C5308.24 3518.67 5198.31 3611.03 5150.95 3650.1C5101.91 3689.18 4990.28 3781.54 4902.34 3856.14C4699.39 4026.65 4406.81 4236.23 4242.76 4330.37C4085.48 4420.95 3767.52 4532.85 3532.44 4582.58C2847.5 4724.67 2054.31 4570.15 1516.5 4190.05C1173.18 3946.72 412.123 3314.41 388.445 3254.02C363.077 3182.98 330.944 3042.66 337.708 3021.35C341.091 3012.47 417.196 3060.42 505.14 3129.69C1056.48 3559.52 1563.85 3863.24 1942.69 3992.9C2328.29 4124.34 2565.06 4163.41 2991.25 4163.41C3380.23 4163.41 3628.84 4126.11 3963.71 4012.44C4345.93 3884.56 4531.96 3781.54 5052.86 3405C5391.11 3161.66 5676.92 2968.06 5700.6 2966.29C5705.68 2966.29 5697.22 3016.02 5678.62 3076.41ZM5426.62 3881C5426.62 3886.33 5409.71 3925.41 5391.11 3966.26C5318.38 4115.45 5144.19 4364.11 5003.81 4518.64C4587.77 4973.33 4090.55 5271.73 3540.9 5392.5C3309.2 5444.01 2708.81 5440.46 2483.88 5387.17C1716.06 5204.23 1105.53 4754.87 696.249 4071.05C647.204 3987.57 609.997 3916.53 613.379 3912.97C616.762 3909.42 774.046 4028.42 965.155 4177.62C1154.57 4326.82 1371.05 4486.67 1443.77 4532.85C1974.82 4863.21 2463.59 4991.09 3118.09 4968C3461.41 4955.57 3691.42 4912.94 3997.53 4806.38C4357.76 4680.27 4623.29 4513.31 5130.66 4095.92C5382.65 3888.11 5426.62 3856.14 5426.62 3881Z',
},
},
],
},
};
}
export async function generateOGImage({
title,
@@ -52,122 +52,122 @@ export async function generateOGImage({
}: GenerateOGImageOptions): Promise<Buffer> {
// Cargar la fuente
const interRegular = await fs.readFile(
join(process.cwd(), "public/fonts/Inter-Regular.ttf"),
);
join(process.cwd(), 'public/fonts/Inter-Regular.ttf'),
)
const interBold = await fs.readFile(
join(process.cwd(), "public/fonts/Inter-Bold.ttf"),
);
join(process.cwd(), 'public/fonts/Inter-Bold.ttf'),
)
// Crear el markup para la imagen OG
const markup = {
type: "div",
type: 'div',
props: {
style: {
height: "100%",
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
justifyContent: "center",
backgroundColor: "#000000",
padding: "80px",
position: "relative",
overflow: "hidden",
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'center',
backgroundColor: '#000000',
padding: '80px',
position: 'relative',
overflow: 'hidden',
},
children: [
{
type: "div",
type: 'div',
props: {
style: {
position: "absolute",
left: "80px",
top: "40px",
fontSize: "32px",
position: 'absolute',
left: '80px',
top: '40px',
fontSize: '32px',
fontWeight: 700,
color: "#fff",
color: '#fff',
zIndex: 1,
},
children: "Dokploy - Blog Post",
children: 'Dokploy - Blog Post',
},
},
{
type: "div",
type: 'div',
props: {
style: {
position: "absolute",
right: "-50px",
bottom: "-50px",
width: "500px",
height: "500px",
position: 'absolute',
right: '-50px',
bottom: '-50px',
width: '500px',
height: '500px',
opacity: 0.1,
display: "flex",
alignItems: "center",
justifyContent: "center",
transform: "rotate(-10deg)",
color: "#ffffff",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transform: 'rotate(-10deg)',
color: '#ffffff',
},
children: DOKPLOY_LOGO,
},
},
{
type: "div",
type: 'div',
props: {
style: {
display: "flex",
flexDirection: "column",
gap: "24px",
position: "relative",
display: 'flex',
flexDirection: 'column',
gap: '24px',
position: 'relative',
zIndex: 1,
},
children: [
{
type: "div",
type: 'div',
props: {
style: {
fontSize: "64px",
fontSize: '64px',
fontWeight: 700,
color: "#fff",
color: '#fff',
lineHeight: 1.2,
maxWidth: "900px",
maxWidth: '900px',
},
children: title,
},
},
{
type: "div",
type: 'div',
props: {
style: {
display: "flex",
alignItems: "center",
gap: "16px",
display: 'flex',
alignItems: 'center',
gap: '16px',
},
children: [
author?.name && {
type: "div",
type: 'div',
props: {
style: {
color: "#9CA3AF",
fontSize: "24px",
color: '#9CA3AF',
fontSize: '24px',
},
children: author.name,
},
},
date && {
type: "div",
type: 'div',
props: {
style: {
color: "#9CA3AF",
fontSize: "24px",
color: '#9CA3AF',
fontSize: '24px',
},
children: `${date}`,
},
},
readingTime && {
type: "div",
type: 'div',
props: {
style: {
color: "#9CA3AF",
fontSize: "24px",
color: '#9CA3AF',
fontSize: '24px',
},
children: `${readingTime} min read`,
},
@@ -180,7 +180,7 @@ export async function generateOGImage({
},
],
},
};
}
// Generar SVG con Satori
const svg = await satori(markup as any, {
@@ -188,22 +188,22 @@ export async function generateOGImage({
height: 630,
fonts: [
{
name: "Inter",
name: 'Inter',
data: interRegular,
weight: 400,
style: "normal",
style: 'normal',
},
{
name: "Inter",
name: 'Inter',
data: interBold,
weight: 700,
style: "normal",
style: 'normal',
},
],
});
})
// Convertir SVG a PNG
const png = await sharp(Buffer.from(svg)).png().toBuffer();
const png = await sharp(Buffer.from(svg)).png().toBuffer()
return png;
return png
}

View File

@@ -1,44 +1,44 @@
declare module "@tryghost/content-api" {
declare module '@tryghost/content-api' {
interface GhostContentAPIOptions {
url: string;
key: string;
version: string;
url: string
key: string
version: string
}
interface BrowseOptions {
limit?: string | number;
page?: number;
order?: string;
filter?: string;
include?: string | string[];
fields?: string | string[];
formats?: string | string[];
limit?: string | number
page?: number
order?: string
filter?: string
include?: string | string[]
fields?: string | string[]
formats?: string | string[]
}
interface ReadOptions {
id?: string;
slug?: string;
include?: string | string[];
fields?: string | string[];
formats?: string | string[];
id?: string
slug?: string
include?: string | string[]
fields?: string | string[]
formats?: string | string[]
}
interface ApiObject {
browse<T>(options?: BrowseOptions): Promise<T[]>;
read<T>(options: ReadOptions): Promise<T>;
browse<T>(options?: BrowseOptions): Promise<T[]>
read<T>(options: ReadOptions): Promise<T>
}
interface GhostAPI {
posts: ApiObject;
tags: ApiObject;
authors: ApiObject;
pages: ApiObject;
posts: ApiObject
tags: ApiObject
authors: ApiObject
pages: ApiObject
settings: {
browse<T>(): Promise<T>;
};
browse<T>(): Promise<T>
}
}
function GhostContentAPI(options: GhostContentAPIOptions): GhostAPI;
function GhostContentAPI(options: GhostContentAPIOptions): GhostAPI
export default GhostContentAPI;
export default GhostContentAPI
}

View File

@@ -1,8 +1,8 @@
"use client";
'use client'
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
return twMerge(clsx(inputs))
}

View File

@@ -1,150 +1,153 @@
import headlessuiPlugin from "@headlessui/tailwindcss";
import type { Config } from "tailwindcss";
import headlessuiPlugin from '@headlessui/tailwindcss'
import type { Config } from 'tailwindcss'
const config = {
darkMode: ["class"],
darkMode: ['class'],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
prefix: '',
theme: {
fontSize: {
xs: ["0.75rem", { lineHeight: "1rem" }],
sm: ["0.875rem", { lineHeight: "1.5rem" }],
base: ["1rem", { lineHeight: "1.75rem" }],
lg: ["1.125rem", { lineHeight: "2rem" }],
xl: ["1.25rem", { lineHeight: "2rem" }],
"2xl": ["1.5rem", { lineHeight: "2rem" }],
"3xl": ["2rem", { lineHeight: "2.5rem" }],
"4xl": ["2.5rem", { lineHeight: "3.5rem" }],
"5xl": ["3rem", { lineHeight: "3.5rem" }],
"6xl": ["3.75rem", { lineHeight: "1" }],
"7xl": ["4.5rem", { lineHeight: "1.1" }],
"8xl": ["6rem", { lineHeight: "1" }],
"9xl": ["8rem", { lineHeight: "1" }],
xs: ['0.75rem', { lineHeight: '1rem' }],
sm: ['0.875rem', { lineHeight: '1.5rem' }],
base: ['1rem', { lineHeight: '1.75rem' }],
lg: ['1.125rem', { lineHeight: '2rem' }],
xl: ['1.25rem', { lineHeight: '2rem' }],
'2xl': ['1.5rem', { lineHeight: '2rem' }],
'3xl': ['2rem', { lineHeight: '2.5rem' }],
'4xl': ['2.5rem', { lineHeight: '3.5rem' }],
'5xl': ['3rem', { lineHeight: '3.5rem' }],
'6xl': ['3.75rem', { lineHeight: '1' }],
'7xl': ['4.5rem', { lineHeight: '1.1' }],
'8xl': ['6rem', { lineHeight: '1' }],
'9xl': ['8rem', { lineHeight: '1' }],
},
container: {
center: true,
padding: "2rem",
padding: '2rem',
screens: {
"2xl": "1400px",
'2xl': '1400px',
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
"4xl": "2rem",
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
'4xl': '2rem',
},
fontFamily: {
sans: "var(--font-inter)",
display: "var(--font-lexend)",
sans: 'var(--font-inter)',
display: 'var(--font-lexend)',
},
keyframes: {
marquee: {
from: {
transform: "translateX(0)",
transform: 'translateX(0)',
},
to: {
transform: "translateX(calc(-100% - var(--gap)))",
transform: 'translateX(calc(-100% - var(--gap)))',
},
},
"marquee-vertical": {
'marquee-vertical': {
from: {
transform: "translateY(0)",
transform: 'translateY(0)',
},
to: {
transform: "translateY(calc(-100% - var(--gap)))",
transform: 'translateY(calc(-100% - var(--gap)))',
},
},
"accordion-down": {
'accordion-down': {
from: {
height: "0",
height: '0',
},
to: {
height: "var(--radix-accordion-content-height)",
height: 'var(--radix-accordion-content-height)',
},
},
"accordion-up": {
'accordion-up': {
from: {
height: "var(--radix-accordion-content-height)",
height: 'var(--radix-accordion-content-height)',
},
to: {
height: "0",
height: '0',
},
},
"shiny-text": {
"0%, 90%, 100%": {
"background-position": "calc(-100% - var(--shiny-width)) 0",
'shiny-text': {
'0%, 90%, 100%': {
'background-position':
'calc(-100% - var(--shiny-width)) 0',
},
"30%, 60%": {
"background-position": "calc(100% + var(--shiny-width)) 0",
'30%, 60%': {
'background-position':
'calc(100% + var(--shiny-width)) 0',
},
},
gradient: {
to: {
backgroundPosition: "var(--bg-size) 0",
backgroundPosition: 'var(--bg-size) 0',
},
},
ripple: {
"0%, 100%": {
transform: "translate(-50%, -50%) scale(1)",
'0%, 100%': {
transform: 'translate(-50%, -50%) scale(1)',
},
"50%": {
transform: "translate(-50%, -50%) scale(0.9)",
'50%': {
transform: 'translate(-50%, -50%) scale(0.9)',
},
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"shiny-text": "shiny-text 8s infinite",
marquee: "marquee var(--duration) linear infinite",
"marquee-vertical": "marquee-vertical var(--duration) linear infinite",
gradient: "gradient 8s linear infinite",
ripple: "ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite",
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
'shiny-text': 'shiny-text 8s infinite',
marquee: 'marquee var(--duration) linear infinite',
'marquee-vertical':
'marquee-vertical var(--duration) linear infinite',
gradient: 'gradient 8s linear infinite',
ripple: 'ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite',
},
},
},
plugins: [require("tailwindcss-animate"), headlessuiPlugin],
} satisfies Config;
plugins: [require('tailwindcss-animate'), headlessuiPlugin],
} satisfies Config
export default config;
export default config