docs: refactor code for consistency and readability

- Standardized import statements across various components to use double quotes for consistency.
- Updated component files to ensure proper formatting and adherence to coding standards.
- Enhanced overall code readability by aligning code structure and improving comment clarity.
- Made minor adjustments to ensure all components follow the same coding conventions, improving maintainability.
This commit is contained in:
Mauricio Siu
2025-12-07 18:13:12 -06:00
parent b605ad10e3
commit 0b52b9b1af
98 changed files with 25084 additions and 27002 deletions

View File

@@ -1,21 +1,21 @@
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server';
import type { InferPageType } from 'fumadocs-core/source';
import { source } from "@/lib/source";
import { createFromSource } from "fumadocs-core/search/server";
import type { InferPageType } from "fumadocs-core/source";
export const { GET } = createFromSource(source, {
// https://docs.orama.com/docs/orama-js/supported-languages
language: 'english',
// Configure tag filter based on the first slug (core, cli, api)
buildIndex(page: InferPageType<typeof source>) {
const tag = page.slugs[0] || 'all';
return {
title: page.data.title,
description: page.data.description,
url: page.url,
id: page.url,
structuredData: page.data.structuredData,
// Assign tag based on the first slug (core, cli, api)
tag,
} as any;
},
// https://docs.orama.com/docs/orama-js/supported-languages
language: "english",
// Configure tag filter based on the first slug (core, cli, api)
buildIndex(page: InferPageType<typeof source>) {
const tag = page.slugs[0] || "all";
return {
title: page.data.title,
description: page.data.description,
url: page.url,
id: page.url,
structuredData: page.data.structuredData,
// Assign tag based on the first slug (core, cli, api)
tag,
} as any;
},
});

View File

@@ -1,54 +1,54 @@
import { getPageImage, source } from '@/lib/source';
import { getPageImage, source } from "@/lib/source";
import { getMDXComponents } from "@/mdx-components";
import {
DocsBody,
DocsDescription,
DocsPage,
DocsTitle,
} from 'fumadocs-ui/layouts/docs/page';
import { notFound } from 'next/navigation';
import { getMDXComponents } from '@/mdx-components';
import type { Metadata } from 'next';
import { createRelativeLink } from 'fumadocs-ui/mdx';
DocsBody,
DocsDescription,
DocsPage,
DocsTitle,
} from "fumadocs-ui/layouts/docs/page";
import { createRelativeLink } from "fumadocs-ui/mdx";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
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={getMDXComponents({
// this allows you to link to other pages with relative file paths
a: createRelativeLink(source, page),
})}
/>
</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={getMDXComponents({
// this allows you to link to other pages with relative file paths
a: createRelativeLink(source, page),
})}
/>
</DocsBody>
</DocsPage>
);
}
export async function generateStaticParams() {
return source.generateParams();
return source.generateParams();
}
export async function generateMetadata(
props: PageProps<'/docs/[[...slug]]'>,
props: PageProps<"/docs/[[...slug]]">,
): Promise<Metadata> {
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,
description: page.data.description,
openGraph: {
images: getPageImage(page).url,
},
};
return {
title: page.data.title,
description: page.data.description,
openGraph: {
images: getPageImage(page).url,
},
};
}

View File

@@ -1,11 +1,11 @@
import { source } from '@/lib/source';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { baseOptions } from '@/lib/layout.shared';
import { baseOptions } from "@/lib/layout.shared";
import { source } from "@/lib/source";
import { DocsLayout } from "fumadocs-ui/layouts/docs";
export default function Layout({ children }: LayoutProps<'/docs'>) {
return (
<DocsLayout tree={source.pageTree} {...baseOptions()}>
{children}
</DocsLayout>
);
export default function Layout({ children }: LayoutProps<"/docs">) {
return (
<DocsLayout tree={source.pageTree} {...baseOptions()}>
{children}
</DocsLayout>
);
}

View File

@@ -1,42 +1,50 @@
import { RootProvider } from 'fumadocs-ui/provider/next';
import './global.css';
import { Inter } from 'next/font/google';
import type { Metadata } from 'next';
import { GoogleAnalytics } from '@next/third-parties/google';
import SearchDialog from '@/components/SearchDialog';
import { RootProvider } from "fumadocs-ui/provider/next";
import "./global.css";
import SearchDialog from "@/components/SearchDialog";
import { GoogleAnalytics } from "@next/third-parties/google";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
const inter = Inter({
subsets: ['latin'],
subsets: ["latin"],
});
export const metadata: Metadata = {
title: {
default: 'Dokploy Documentation',
template: '%s | Dokploy',
},
description: 'Open Source Alternative to Vercel, Netlify and Heroku. Deploy your applications with ease.',
keywords: ['dokploy', 'deployment', 'docker', 'hosting', 'devops', 'open source'],
authors: [{ name: 'Dokploy Team' }],
openGraph: {
title: 'Dokploy Documentation',
description: 'Open Source Alternative to Vercel, Netlify and Heroku',
type: 'website',
},
title: {
default: "Dokploy Documentation",
template: "%s | Dokploy",
},
description:
"Open Source Alternative to Vercel, Netlify and Heroku. Deploy your applications with ease.",
keywords: [
"dokploy",
"deployment",
"docker",
"hosting",
"devops",
"open source",
],
authors: [{ name: "Dokploy Team" }],
openGraph: {
title: "Dokploy Documentation",
description: "Open Source Alternative to Vercel, Netlify and Heroku",
type: "website",
},
};
export default function Layout({ children }: LayoutProps<'/'>) {
return (
<html lang="en" className={inter.className} suppressHydrationWarning>
<body className="flex flex-col min-h-screen">
<GoogleAnalytics gaId="G-HZ71HG38HN" />
<RootProvider
search={{
SearchDialog,
}}
>
{children}
</RootProvider>
</body>
</html>
);
export default function Layout({ children }: LayoutProps<"/">) {
return (
<html lang="en" className={inter.className} suppressHydrationWarning>
<body className="flex flex-col min-h-screen">
<GoogleAnalytics gaId="G-HZ71HG38HN" />
<RootProvider
search={{
SearchDialog,
}}
>
{children}
</RootProvider>
</body>
</html>
);
}

View File

@@ -1,10 +1,10 @@
import { getLLMText, source } from '@/lib/source';
import { getLLMText, source } from "@/lib/source";
export const revalidate = false;
export async function GET() {
const scan = source.getPages().map(getLLMText);
const scanned = await Promise.all(scan);
const scan = source.getPages().map(getLLMText);
const scanned = await Promise.all(scan);
return new Response(scanned.join('\n\n'));
return new Response(scanned.join("\n\n"));
}

View File

@@ -1,34 +1,34 @@
import { getPageImage, source } from '@/lib/source';
import { notFound } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { generate as DefaultImage } from 'fumadocs-ui/og';
import { getPageImage, source } from "@/lib/source";
import { generate as DefaultImage } from "fumadocs-ui/og";
import { notFound } from "next/navigation";
import { ImageResponse } from "next/og";
export const revalidate = false;
export async function GET(
_req: Request,
{ params }: RouteContext<'/og/docs/[...slug]'>,
_req: Request,
{ params }: RouteContext<"/og/docs/[...slug]">,
) {
const { slug } = await params;
const page = source.getPage(slug.slice(0, -1));
if (!page) notFound();
const { slug } = await params;
const page = source.getPage(slug.slice(0, -1));
if (!page) notFound();
return new ImageResponse(
<DefaultImage
title={page.data.title}
description={page.data.description}
site="My App"
/>,
{
width: 1200,
height: 630,
},
);
return new ImageResponse(
<DefaultImage
title={page.data.title}
description={page.data.description}
site="My App"
/>,
{
width: 1200,
height: 630,
},
);
}
export function generateStaticParams() {
return source.getPages().map((page) => ({
lang: page.locale,
slug: getPageImage(page).segments,
}));
return source.getPages().map((page) => ({
lang: page.locale,
slug: getPageImage(page).segments,
}));
}

View File

@@ -1,22 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/global.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/global.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}

View File

@@ -1,6 +1,6 @@
import type { ComponentPropsWithoutRef } from 'react';
import type { ComponentPropsWithoutRef } from "react";
export function Logo(props: ComponentPropsWithoutRef<'svg'>) {
export function Logo(props: ComponentPropsWithoutRef<"svg">) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -23,5 +23,5 @@ export function Logo(props: 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,95 +1,95 @@
import { Menu } from 'lucide-react';
import { LogIn, UserPlus, Globe, Heart, Github, Rss } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Menu } from "lucide-react";
import { Github, Globe, Heart, LogIn, Rss, UserPlus } from "lucide-react";
export function NavLinks() {
const links = [
{
text: 'Login',
url: 'https://app.dokploy.com/',
icon: LogIn,
},
{
text: 'Sign Up',
url: 'https://app.dokploy.com/register',
icon: UserPlus,
},
{
text: 'Website',
url: 'https://dokploy.com',
icon: Globe,
},
{
text: 'Discord',
url: 'https://discord.com/invite/2tBnJ3jDJc',
icon: () => (
<svg
role="img"
className="size-4"
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',
icon: Heart,
},
{
text: 'Github',
url: 'https://github.com/dokploy/dokploy',
icon: Github,
},
{
text: 'Blog',
url: 'https://dokploy.com/blog',
icon: Rss,
},
];
const links = [
{
text: "Login",
url: "https://app.dokploy.com/",
icon: LogIn,
},
{
text: "Sign Up",
url: "https://app.dokploy.com/register",
icon: UserPlus,
},
{
text: "Website",
url: "https://dokploy.com",
icon: Globe,
},
{
text: "Discord",
url: "https://discord.com/invite/2tBnJ3jDJc",
icon: () => (
<svg
role="img"
className="size-4"
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",
icon: Heart,
},
{
text: "Github",
url: "https://github.com/dokploy/dokploy",
icon: Github,
},
{
text: "Blog",
url: "https://dokploy.com/blog",
icon: Rss,
},
];
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="inline-flex items-center justify-center rounded-md p-2 hover:bg-fd-accent hover:text-fd-accent-foreground transition-colors"
aria-label="Quick links menu"
>
<Menu className="size-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
{links.map((link, index) => {
const IconComponent = link.icon;
const showSeparator = index === 2;
return (
<div key={link.text}>
<DropdownMenuItem asChild>
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 cursor-pointer"
>
<IconComponent className="size-4 text-fd-muted-foreground" />
<span>{link.text}</span>
</a>
</DropdownMenuItem>
{showSeparator && <DropdownMenuSeparator />}
</div>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="inline-flex items-center justify-center rounded-md p-2 hover:bg-fd-accent hover:text-fd-accent-foreground transition-colors"
aria-label="Quick links menu"
>
<Menu className="size-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
{links.map((link, index) => {
const IconComponent = link.icon;
const showSeparator = index === 2;
return (
<div key={link.text}>
<DropdownMenuItem asChild>
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 cursor-pointer"
>
<IconComponent className="size-4 text-fd-muted-foreground" />
<span>{link.text}</span>
</a>
</DropdownMenuItem>
{showSeparator && <DropdownMenuSeparator />}
</div>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -1,54 +1,54 @@
'use client';
"use client";
import { useDocsSearch } from "fumadocs-core/search/client";
import {
SearchDialog,
SearchDialogClose,
SearchDialogContent,
SearchDialogFooter,
SearchDialogHeader,
SearchDialogIcon,
SearchDialogInput,
SearchDialogList,
SearchDialogOverlay,
TagsList,
TagsListItem,
type SharedProps,
} from 'fumadocs-ui/components/dialog/search';
import { useDocsSearch } from 'fumadocs-core/search/client';
import { useState } from 'react';
SearchDialog,
SearchDialogClose,
SearchDialogContent,
SearchDialogFooter,
SearchDialogHeader,
SearchDialogIcon,
SearchDialogInput,
SearchDialogList,
SearchDialogOverlay,
type SharedProps,
TagsList,
TagsListItem,
} from "fumadocs-ui/components/dialog/search";
import { useState } from "react";
export default function CustomSearchDialog(props: SharedProps) {
const [tag, setTag] = useState<string | undefined>('all');
// When tag is "all", don't filter by tag (pass undefined)
const { search, setSearch, query } = useDocsSearch({
type: 'fetch',
tag: tag === 'all' ? undefined : tag,
});
const [tag, setTag] = useState<string | undefined>("all");
// When tag is "all", don't filter by tag (pass undefined)
const { search, setSearch, query } = useDocsSearch({
type: "fetch",
tag: tag === "all" ? undefined : tag,
});
return (
<SearchDialog
search={search}
onSearchChange={setSearch}
isLoading={query.isLoading}
{...props}
>
<SearchDialogOverlay />
<SearchDialogContent>
<SearchDialogHeader>
<SearchDialogIcon />
<SearchDialogInput />
<SearchDialogClose />
</SearchDialogHeader>
<SearchDialogList items={query.data !== 'empty' ? query.data : null} />
<SearchDialogFooter>
<TagsList tag={tag} onTagChange={setTag}>
<TagsListItem value="all">All</TagsListItem>
<TagsListItem value="core">Core</TagsListItem>
<TagsListItem value="cli">CLI</TagsListItem>
<TagsListItem value="api">API</TagsListItem>
</TagsList>
</SearchDialogFooter>
</SearchDialogContent>
</SearchDialog>
);
return (
<SearchDialog
search={search}
onSearchChange={setSearch}
isLoading={query.isLoading}
{...props}
>
<SearchDialogOverlay />
<SearchDialogContent>
<SearchDialogHeader>
<SearchDialogIcon />
<SearchDialogInput />
<SearchDialogClose />
</SearchDialogHeader>
<SearchDialogList items={query.data !== "empty" ? query.data : null} />
<SearchDialogFooter>
<TagsList tag={tag} onTagChange={setTag}>
<TagsListItem value="all">All</TagsListItem>
<TagsListItem value="core">Core</TagsListItem>
<TagsListItem value="cli">CLI</TagsListItem>
<TagsListItem value="api">API</TagsListItem>
</TagsList>
</SearchDialogFooter>
</SearchDialogContent>
</SearchDialog>
);
}

View File

@@ -1,257 +1,257 @@
"use client"
"use client";
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import type * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function DropdownMenu({
...props
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({
...props
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
);
}
function DropdownMenuTrigger({
...props
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
);
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
function DropdownMenuGroup({
...props
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({
...props
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
function DropdownMenuRadioItem({
className,
children,
...props
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
function DropdownMenuLabel({
className,
inset,
...props
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({
className,
...props
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
function DropdownMenuSub({
...props
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};

View File

@@ -38,20 +38,20 @@
"auto-deploy",
"schedule-jobs",
"volume-backups",
"providers",
"watch-paths",
"---Remote Servers---",
"remote-servers/index",
"remote-servers/instructions",
"remote-servers/build-server",
"remote-servers/deployments",
"remote-servers/security",
"remote-servers/validate",
"---Guides---",
"guides/cloudflare-tunnels",
"guides/tailscale",
"guides/ec2-instructions",
"---Advanced---",
"cluster"
]
"providers",
"watch-paths",
"---Remote Servers---",
"remote-servers/index",
"remote-servers/instructions",
"remote-servers/build-server",
"remote-servers/deployments",
"remote-servers/security",
"remote-servers/validate",
"---Guides---",
"guides/cloudflare-tunnels",
"guides/tailscale",
"guides/ec2-instructions",
"---Advanced---",
"cluster"
]
}

View File

@@ -1,19 +1,19 @@
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
import { Logo } from '@/components/Logo';
import { NavLinks } from '@/components/NavLinks';
import { Logo } from "@/components/Logo";
import { NavLinks } from "@/components/NavLinks";
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
export function baseOptions(): BaseLayoutProps {
return {
nav: {
title: (
<div className="flex items-center gap-2">
<Logo />
<span>Dokploy</span>
</div>
),
url: 'https://dokploy.com',
children: <NavLinks />,
},
githubUrl: 'https://github.com/Dokploy/dokploy',
};
return {
nav: {
title: (
<div className="flex items-center gap-2">
<Logo />
<span>Dokploy</span>
</div>
),
url: "https://dokploy.com",
children: <NavLinks />,
},
githubUrl: "https://github.com/Dokploy/dokploy",
};
}

View File

@@ -1,35 +1,35 @@
import { docs } from 'fumadocs-mdx:collections/server';
import { type InferPageType, loader } from 'fumadocs-core/source';
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
import { createOpenAPI } from 'fumadocs-openapi/server';
import { createAPIPage } from 'fumadocs-openapi/ui';
import { docs } from "fumadocs-mdx:collections/server";
import { type InferPageType, loader } from "fumadocs-core/source";
import { lucideIconsPlugin } from "fumadocs-core/source/lucide-icons";
import { createOpenAPI } from "fumadocs-openapi/server";
import { createAPIPage } from "fumadocs-openapi/ui";
// See https://fumadocs.dev/docs/headless/source-api for more info
export const source = loader({
baseUrl: '/docs',
source: docs.toFumadocsSource(),
plugins: [lucideIconsPlugin()],
baseUrl: "/docs",
source: docs.toFumadocsSource(),
plugins: [lucideIconsPlugin()],
});
export const openapi = createOpenAPI({
input: ['./public/openapi.json'],
input: ["./public/openapi.json"],
});
export const APIPage = createAPIPage(openapi);
export function getPageImage(page: InferPageType<typeof source>) {
const segments = [...page.slugs, 'image.png'];
const segments = [...page.slugs, "image.png"];
return {
segments,
url: `/og/docs/${segments.join('/')}`,
};
return {
segments,
url: `/og/docs/${segments.join("/")}`,
};
}
export async function getLLMText(page: InferPageType<typeof source>) {
const processed = await page.data.getText('processed');
const processed = await page.data.getText("processed");
return `# ${page.data.title}
return `# ${page.data.title}
${processed}`;
}

View File

@@ -1,6 +1,6 @@
import { clsx, type ClassValue } 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,28 +1,23 @@
import defaultMdxComponents from 'fumadocs-ui/mdx';
import type { MDXComponents } from 'mdx/types';
import { ImageZoom } from 'fumadocs-ui/components/image-zoom';
import { Callout } from 'fumadocs-ui/components/callout';
import { APIPage } from '@/lib/source';
import { APIPage } from "@/lib/source";
import { Callout } from "fumadocs-ui/components/callout";
import { ImageZoom } from "fumadocs-ui/components/image-zoom";
import defaultMdxComponents from "fumadocs-ui/mdx";
import type { MDXComponents } from "mdx/types";
export function getMDXComponents(components?: MDXComponents): MDXComponents {
return {
...defaultMdxComponents,
ImageZoom,
Callout,
APIPage,
...components,
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>
),
};
return {
...defaultMdxComponents,
ImageZoom,
Callout,
APIPage,
...components,
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>
),
};
}

View File

@@ -1,11 +1,10 @@
import { createMDX } from 'fumadocs-mdx/next';
import { createMDX } from "fumadocs-mdx/next";
const withMDX = createMDX();
/** @type {import('next').NextConfig} */
const config = {
reactStrictMode: true,
reactStrictMode: true,
};
export default withMDX(config);

View File

@@ -1,41 +1,41 @@
{
"name": "docs",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev",
"start": "next start",
"types:check": "fumadocs-mdx && tsc --noEmit",
"postinstall": "fumadocs-mdx",
"fix-openapi": "node scripts/fix-openapi.mjs",
"build:docs": "npm run fix-openapi && node generate-docs.mjs"
},
"dependencies": {
"@next/third-parties": "16.0.7",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"fumadocs-core": "16.2.3",
"fumadocs-mdx": "14.1.0",
"fumadocs-openapi": "10.1.1",
"fumadocs-ui": "16.2.3",
"lucide-react": "^0.552.0",
"next": "16.0.7",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"shiki": "3.19.0",
"tailwind-merge": "^2.5.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.16",
"@types/mdx": "^2.0.13",
"@types/node": "^24.10.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.16",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3"
}
}
"name": "docs",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev",
"start": "next start",
"types:check": "fumadocs-mdx && tsc --noEmit",
"postinstall": "fumadocs-mdx",
"fix-openapi": "node scripts/fix-openapi.mjs",
"build:docs": "npm run fix-openapi && node generate-docs.mjs"
},
"dependencies": {
"@next/third-parties": "16.0.7",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"fumadocs-core": "16.2.3",
"fumadocs-mdx": "14.1.0",
"fumadocs-openapi": "10.1.1",
"fumadocs-ui": "16.2.3",
"lucide-react": "^0.552.0",
"next": "16.0.7",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"shiki": "3.19.0",
"tailwind-merge": "^2.5.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.16",
"@types/mdx": "^2.0.13",
"@types/node": "^24.10.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.16",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3"
}
}

View File

@@ -1,5 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
plugins: {
"@tailwindcss/postcss": {},
},
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,16 @@
import { readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { readFileSync, writeFileSync } from "fs";
import { join } from "path";
const openapiPath = join(process.cwd(), 'public', 'openapi.json');
const openapiPath = join(process.cwd(), "public", "openapi.json");
console.log('Fixing OpenAPI schema...');
console.log("Fixing OpenAPI schema...");
try {
const openapi = JSON.parse(readFileSync(openapiPath, 'utf8'));
const openapi = JSON.parse(readFileSync(openapiPath, "utf8"));
let fixed = 0;
let securityFixed = false;
// Remove Authorization security scheme and add x-api-key
if (!openapi.components) {
openapi.components = {};
@@ -18,70 +18,66 @@ try {
if (!openapi.components.securitySchemes) {
openapi.components.securitySchemes = {};
}
// Remove old Authorization scheme
if (openapi.components.securitySchemes['Authorization']) {
delete openapi.components.securitySchemes['Authorization'];
if (openapi.components.securitySchemes["Authorization"]) {
delete openapi.components.securitySchemes["Authorization"];
securityFixed = true;
}
// Add x-api-key scheme
openapi.components.securitySchemes['x-api-key'] = {
type: 'apiKey',
in: 'header',
name: 'x-api-key',
description: 'API key authentication. Use YOUR-GENERATED-API-KEY',
'x-default': 'your-key'
openapi.components.securitySchemes["x-api-key"] = {
type: "apiKey",
in: "header",
name: "x-api-key",
description: "API key authentication. Use YOUR-GENERATED-API-KEY",
"x-default": "your-key",
};
securityFixed = true;
// Replace global security from Authorization to x-api-key
if (openapi.security) {
openapi.security = openapi.security.filter(
sec => !sec['Authorization']
);
openapi.security = openapi.security.filter((sec) => !sec["Authorization"]);
} else {
openapi.security = [];
}
const hasApiKeySecurity = openapi.security.some(
sec => sec['x-api-key']
);
const hasApiKeySecurity = openapi.security.some((sec) => sec["x-api-key"]);
if (!hasApiKeySecurity) {
openapi.security.push({ 'x-api-key': [] });
openapi.security.push({ "x-api-key": [] });
securityFixed = true;
}
// Replace Authorization with x-api-key in all operation security
for (const [path, pathItem] of Object.entries(openapi.paths || {})) {
for (const [method, operation] of Object.entries(pathItem)) {
if (operation && operation.security) {
// Replace Authorization with x-api-key
operation.security = operation.security.map(sec => {
if (sec['Authorization'] !== undefined) {
operation.security = operation.security.map((sec) => {
if (sec["Authorization"] !== undefined) {
securityFixed = true;
return { 'x-api-key': [] };
return { "x-api-key": [] };
}
return sec;
});
}
}
}
// Fix empty response schemas
for (const [path, pathItem] of Object.entries(openapi.paths || {})) {
for (const [method, operation] of Object.entries(pathItem)) {
if (operation.responses) {
for (const [status, response] of Object.entries(operation.responses)) {
if (response.content && response.content['application/json']) {
const content = response.content['application/json'];
if (response.content && response.content["application/json"]) {
const content = response.content["application/json"];
// Check if schema is completely empty or missing
if (Object.keys(content).length === 0 || !content.schema) {
response.content['application/json'] = {
response.content["application/json"] = {
schema: {
type: 'object',
description: 'Successful response'
}
type: "object",
description: "Successful response",
},
};
fixed++;
}
@@ -96,10 +92,9 @@ try {
if (fixed > 0) console.log(`✓ Fixed ${fixed} empty response schemas`);
if (securityFixed) console.log("✓ Added x-api-key security scheme");
} else {
console.log('✓ No fixes needed');
console.log("✓ No fixes needed");
}
} catch (error) {
console.error('Error fixing OpenAPI schema:', error.message);
console.error("Error fixing OpenAPI schema:", error.message);
process.exit(1);
}

View File

@@ -1,27 +1,27 @@
import {
defineConfig,
defineDocs,
frontmatterSchema,
metaSchema,
} from 'fumadocs-mdx/config';
defineConfig,
defineDocs,
frontmatterSchema,
metaSchema,
} from "fumadocs-mdx/config";
// You can customise Zod schemas for frontmatter and `meta.json` here
// see https://fumadocs.dev/docs/mdx/collections
export const docs = defineDocs({
dir: 'content/docs',
docs: {
schema: frontmatterSchema,
postprocess: {
includeProcessedMarkdown: true,
},
},
meta: {
schema: metaSchema,
},
dir: "content/docs",
docs: {
schema: frontmatterSchema,
postprocess: {
includeProcessedMarkdown: true,
},
},
meta: {
schema: metaSchema,
},
});
export default defineConfig({
mdxOptions: {
// MDX options
},
mdxOptions: {
// MDX options
},
});

View File

@@ -1,36 +1,36 @@
{
"compilerOptions": {
"baseUrl": ".",
"target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"paths": {
"@/*": ["./*"],
"fumadocs-mdx:collections/*": [".source/*"]
},
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
"compilerOptions": {
"baseUrl": ".",
"target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"paths": {
"@/*": ["./*"],
"fumadocs-mdx:collections/*": [".source/*"]
},
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}

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 { getHubSpotUTK, submitToHubSpot } from "@/lib/hubspot";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { Resend } from "resend";
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,45 +37,41 @@ 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:
@@ -90,23 +86,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},
@@ -126,24 +122,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,19 +1,19 @@
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
@@ -22,11 +22,10 @@ export async function GET(request: Request) {
{ 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 {
@@ -34,42 +33,41 @@ 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,34 +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,
@@ -40,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,32 +43,32 @@ 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 (
@@ -78,7 +78,7 @@ export function CodeBlock({ code, lang, initial }: CodeBlockProps) {
<div className="h-4 w-1/2 rounded bg-gray-700" />
</div>
</div>
)
);
}
return (
@@ -88,5 +88,5 @@ export function CodeBlock({ code, lang, initial }: CodeBlockProps) {
{nodes}
</div>
</div>
)
);
}

View File

@@ -1,13 +1,13 @@
'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 (
@@ -24,18 +24,18 @@ 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() || '', {
const router = useRouter();
const id = slugify(children?.toString() || "", {
lower: true,
strict: true,
})
});
const handleClick = () => {
router.push(`#${id}`)
}
router.push(`#${id}`);
};
return (
<h1
id={id}
@@ -46,18 +46,18 @@ export function H1({ children, ...props }: HeadingProps) {
{children}
<LinkIcon />
</h1>
)
);
}
export function H2({ children, ...props }: HeadingProps) {
const router = useRouter()
const id = slugify(children?.toString() || '', {
const router = useRouter();
const id = slugify(children?.toString() || "", {
lower: true,
strict: true,
})
});
const handleClick = () => {
router.push(`#${id}`)
}
router.push(`#${id}`);
};
return (
<h2
id={id}
@@ -68,18 +68,18 @@ export function H2({ children, ...props }: HeadingProps) {
{children}
<LinkIcon />
</h2>
)
);
}
export function H3({ children, ...props }: HeadingProps) {
const router = useRouter()
const id = slugify(children?.toString() || '', {
const router = useRouter();
const id = slugify(children?.toString() || "", {
lower: true,
strict: true,
})
});
const handleClick = () => {
router.push(`#${id}`)
}
router.push(`#${id}`);
};
return (
<h3
id={id}
@@ -90,5 +90,5 @@ export function H3({ children, ...props }: HeadingProps) {
{children}
<LinkIcon />
</h3>
)
);
}

View File

@@ -1,45 +1,45 @@
'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">
@@ -56,12 +56,12 @@ export function TableOfContents() {
<a
href={`#${heading.id}`}
onClick={(e) => {
e.preventDefault()
e.preventDefault();
document
.getElementById(heading.id)
?.scrollIntoView({ behavior: 'smooth' })
?.scrollIntoView({ behavior: "smooth" });
}}
className={`block transition-colors hover:text-primary ${activeId === heading.id ? 'font-medium text-primary' : 'text-muted-foreground'}`}
className={`block transition-colors hover:text-primary ${activeId === heading.id ? "font-medium text-primary" : "text-muted-foreground"}`}
>
{heading.text}
</a>
@@ -69,12 +69,10 @@ 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,25 +1,21 @@
'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)
"/api/og",
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,
@@ -186,8 +186,8 @@ export default async function BlogPostPage({ params }: Props) {
),
img: ({ node, src, alt }) => (
<ZoomableImage
src={src || ''}
alt={alt || ''}
src={src || ""}
alt={alt || ""}
className="mx-auto max-w-lg overflow-hidden rounded-lg border border-border object-cover max-lg:w-64"
/>
),
@@ -196,26 +196,26 @@ export default async function BlogPostPage({ params }: Props) {
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="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 w-full max-w-7xl px-4 pb-12 sm:px-6 lg:px-8">
@@ -255,20 +255,14 @@ 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"
/>
@@ -284,17 +278,14 @@ export default async function BlogPostPage({ params }: Props) {
rel="noopener noreferrer"
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>
@@ -354,11 +345,11 @@ export default async function BlogPostPage({ params }: Props) {
{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
@@ -370,10 +361,7 @@ export default async function BlogPostPage({ params }: Props) {
{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 "
/>
@@ -384,9 +372,7 @@ export default async function BlogPostPage({ params }: Props) {
{relatedPost.title}
</h3>
<p className="mb-4 text-sm text-muted-foreground">
{relatedPostDate} {' '}
{relatedPost.reading_time} min
read
{relatedPostDate} {relatedPost.reading_time} min read
</p>
<p className="line-clamp-2 text-muted-foreground">
{relatedPost.excerpt}
@@ -394,11 +380,11 @@ export default async function BlogPostPage({ params }: Props) {
</div>
</div>
</Link>
)
);
})}
</div>
</div>
)}
</article>
)
);
}

View File

@@ -1,28 +1,28 @@
'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
@@ -32,8 +32,8 @@ export function BlogPostCard({ post }: BlogPostCardProps) {
<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'}
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>
@@ -55,20 +55,14 @@ 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"
/>
@@ -81,18 +75,14 @@ export function BlogPostCard({ post }: BlogPostCardProps) {
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>
@@ -101,5 +91,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,39 +35,39 @@ 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="mb-8 flex flex-col gap-4 md:flex-row">
@@ -92,9 +92,7 @@ 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}
@@ -104,5 +102,5 @@ export function SearchAndFilter({
</Select>
</div>
</div>
)
);
}

View File

@@ -1,47 +1,46 @@
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 max-w-5xl px-4 py-12">
@@ -50,9 +49,7 @@ export default async function BlogPage({
<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"
@@ -74,8 +71,8 @@ export default async function BlogPage({
<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'}
? "No posts found matching your criteria"
: "No posts available"}
</p>
</div>
) : (
@@ -86,5 +83,5 @@ export default async function BlogPage({
</div>
)}
</div>
)
);
}

View File

@@ -1,51 +1,49 @@
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">
@@ -70,11 +68,11 @@ export default async function TagPage({ params }: Props) {
<div className="mb-8">
<h1 className="mb-2 text-3xl font-bold">
Posts tagged with{' '}
Posts tagged with{" "}
<span className="text-primary-600">"{tagName}"</span>
</h1>
<p className="text-gray-600 dark:text-gray-400">
{posts.length} {posts.length === 1 ? 'post' : 'posts'} found
{posts.length} {posts.length === 1 ? "post" : "posts"} found
</p>
</div>
@@ -84,15 +82,15 @@ export default async function TagPage({ params }: Props) {
))}
</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">
@@ -130,12 +128,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,12 +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}</>
return <>{children}</>;
}

View File

@@ -1,144 +1,147 @@
'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 { Container } from "@/components/Container";
import { trackGAEvent } from "@/components/analitycs";
import AnimatedGridPattern from "@/components/ui/animated-grid-pattern";
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 { cn } from "@/lib/utils";
import { useState } from "react";
interface ContactFormData {
inquiryType: '' | 'support' | 'sales' | 'other'
deploymentType: '' | 'cloud' | 'self-hosted'
firstName: string
lastName: string
email: string
company: string
message: string
inquiryType: "" | "support" | "sales" | "other";
deploymentType: "" | "cloud" | "self-hosted";
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: '',
deploymentType: '',
firstName: '',
lastName: '',
email: '',
company: '',
message: '',
})
const [errors, setErrors] = useState<Record<string, string>>({})
inquiryType: "",
deploymentType: "",
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.inquiryType === 'support' && !formData.deploymentType) {
newErrors.deploymentType = 'Please select your deployment type'
if (formData.inquiryType === "support" && !formData.deploymentType) {
newErrors.deploymentType = "Please select your deployment type";
}
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;
}
// Prevent submission for self-hosted support requests
if (formData.inquiryType === 'support' && formData.deploymentType === 'self-hosted') {
return
if (
formData.inquiryType === "support" &&
formData.deploymentType === "self-hosted"
) {
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: '',
deploymentType: '',
firstName: '',
lastName: '',
email: '',
company: '',
message: '',
})
setErrors({})
setIsSubmitted(true)
inquiryType: "",
deploymentType: "",
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) => {
const updated = { ...prev, [field]: value }
const updated = { ...prev, [field]: value };
// Reset deploymentType when inquiryType changes and is not support
if (field === 'inquiryType' && value !== 'support') {
updated.deploymentType = ''
if (field === "inquiryType" && value !== "support") {
updated.deploymentType = "";
}
return updated
})
return updated;
});
if (errors[field]) {
setErrors((prev) => {
const newErrors = { ...prev }
delete newErrors[field]
return newErrors
})
const newErrors = { ...prev };
delete newErrors[field];
return newErrors;
});
}
}
};
if (isSubmitted) {
return (
@@ -149,21 +152,18 @@ 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 (
@@ -176,8 +176,8 @@ 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>
@@ -187,8 +187,8 @@ export default function ContactPage() {
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>
@@ -198,15 +198,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",
)
}
>
@@ -214,35 +214,31 @@ 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>
{formData.inquiryType === 'support' && (
{formData.inquiryType === "support" && (
<div className="space-y-2">
<label
htmlFor="deploymentType"
className="block text-sm font-medium text-foreground"
>
What version of Dokploy are you using?{' '}
What version of Dokploy are you using?{" "}
<span className="text-red-500">*</span>
</label>
<Select
value={formData.deploymentType}
onValueChange={(value) =>
handleInputChange(
'deploymentType',
value as 'cloud' | 'self-hosted',
"deploymentType",
value as "cloud" | "self-hosted",
)
}
>
@@ -250,12 +246,8 @@ export default function ContactPage() {
<SelectValue placeholder="Select deployment type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="cloud">
Cloud Version
</SelectItem>
<SelectItem value="self-hosted">
Self Hosted
</SelectItem>
<SelectItem value="cloud">Cloud Version</SelectItem>
<SelectItem value="self-hosted">Self Hosted</SelectItem>
</SelectContent>
</Select>
{errors.deploymentType && (
@@ -264,13 +256,15 @@ export default function ContactPage() {
</p>
)}
{formData.deploymentType === 'self-hosted' && (
{formData.deploymentType === "self-hosted" && (
<div className="mt-4 rounded-lg border border-amber-500/50 bg-amber-500/10 p-4">
<h3 className="mb-2 text-sm font-semibold text-amber-500">
Self-Hosted Support
</h3>
<p className="mb-3 text-sm text-muted-foreground">
We currently don't provide direct support for self-hosted deployments through this form. However, our community is here to help!
We currently don't provide direct support for self-hosted
deployments through this form. However, our community is
here to help!
</p>
<div className="space-y-2 text-sm">
<p className="text-muted-foreground">
@@ -278,7 +272,7 @@ export default function ContactPage() {
</p>
<ul className="ml-4 list-disc space-y-1 text-muted-foreground">
<li>
Join our{' '}
Join our{" "}
<a
href="https://discord.gg/2tBnJ3jDJc"
target="_blank"
@@ -286,11 +280,11 @@ export default function ContactPage() {
className="text-primary underline hover:text-primary/80"
>
Discord community
</a>{' '}
</a>{" "}
for real-time help
</li>
<li>
Open a discussion on{' '}
Open a discussion on{" "}
<a
href="https://github.com/Dokploy/dokploy/discussions"
target="_blank"
@@ -313,25 +307,19 @@ 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>
@@ -340,25 +328,19 @@ 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>
@@ -374,15 +356,11 @@ 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>
@@ -391,22 +369,17 @@ 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>
@@ -415,23 +388,18 @@ 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 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>
@@ -440,17 +408,17 @@ export default function ContactPage() {
type="submit"
disabled={
isSubmitting ||
(formData.inquiryType === 'support' &&
formData.deploymentType === 'self-hosted')
(formData.inquiryType === "support" &&
formData.deploymentType === "self-hosted")
}
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 { GoogleAnalytics } from "@next/third-parties/google";
import clsx from "clsx";
import type { Metadata } from "next";
import { Inter, Lexend } from "next/font/google";
import type { ReactNode } from "react";
import "@/styles/tailwind.css";
import "react-photo-view/dist/react-photo-view.css";
import { Footer } from "@/components/Footer";
import { Header } from "@/components/Header";
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,21 +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',
}
"Open-source self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases",
};
export default function Home() {
return (
@@ -34,5 +34,5 @@ export default function Home() {
<CallToAction />
</main>
</div>
)
);
}

View File

@@ -1,10 +1,10 @@
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 (
@@ -13,31 +13,25 @@ export default function PrivacyPage() {
<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="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="mb-4 list-inside list-disc">
<li>
Website usage statistics (e.g., page views, session
duration)
</li>
<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>
@@ -50,8 +44,8 @@ export default function PrivacyPage() {
</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="mb-4 list-inside list-disc">
<li>Monitor traffic and website performance</li>
@@ -59,42 +53,37 @@ export default function PrivacyPage() {
<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="mb-4 text-2xl font-semibold">
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="mb-4 text-2xl font-semibold">
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="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>
@@ -103,22 +92,20 @@ export default function PrivacyPage() {
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="mb-4 text-2xl font-semibold">
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"
@@ -128,5 +115,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,10 +1,10 @@
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 (
@@ -15,14 +15,13 @@ export default function TermsPage() {
<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="mb-4 text-2xl font-semibold">1. Definitions</h2>
<p className="">
@@ -36,27 +35,25 @@ 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="mb-4 text-2xl font-semibold">
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>
@@ -65,18 +62,17 @@ export default function TermsPage() {
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>
@@ -86,29 +82,25 @@ export default function TermsPage() {
</h2>
<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="mb-4 text-2xl font-semibold">
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>
@@ -117,24 +109,22 @@ export default function TermsPage() {
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>
@@ -143,11 +133,10 @@ export default function TermsPage() {
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>
@@ -156,69 +145,61 @@ export default function TermsPage() {
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="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="mb-4 text-2xl font-semibold">
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="mb-4 text-2xl font-semibold">
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="mb-4 text-2xl font-semibold">
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"
@@ -228,5 +209,5 @@ export default function TermsPage() {
</p>
</section>
</div>
)
);
}

File diff suppressed because one or more lines are too long

View File

@@ -1,16 +1,13 @@
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,80 +3,95 @@ 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?',
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 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.',
"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 (
@@ -94,9 +109,8 @@ export function Faqs() {
Frequently asked questions
</h2>
<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.
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>
@@ -106,10 +120,7 @@ export function Faqs() {
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>
@@ -119,5 +130,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 (
@@ -87,5 +87,5 @@ export function Footer() {
</div>
</Container>
</footer>
)
);
}

View File

@@ -1,79 +1,77 @@
'use client'
"use client";
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils";
import Link from "next/link";
import { useEffect, useState } from "react";
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
@@ -81,11 +79,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,
)}
>
@@ -98,9 +96,9 @@ export function GithubStars({
<svg
viewBox="0 0 24 24"
className={cn(
'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]',
"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"
>
@@ -110,9 +108,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-delay:.7s] [animation-duration:1.9s]',
"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"
>
@@ -122,9 +120,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-delay:1.1s] [animation-duration:2.2s]',
"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"
>
@@ -139,10 +137,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>
@@ -150,9 +148,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
@@ -169,7 +167,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>
@@ -177,12 +175,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 { cn } from "@/lib/utils";
import { Popover, Transition } from "@headlessui/react";
import { ChevronRight, HeartIcon } from "lucide-react";
import Link from "next/link";
import { Fragment, type JSX, type SVGProps } from "react";
import { Container } from "./Container";
import GithubStars from "./GithubStars";
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";
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,20 +52,17 @@ 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>) => (
@@ -84,7 +81,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 (
@@ -152,7 +149,7 @@ function MobileNavigation() {
</Transition.Child>
</Transition.Root>
</Popover>
)
);
}
export function Header() {
@@ -201,10 +198,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
@@ -244,5 +241,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">
@@ -84,7 +84,7 @@ export function Hero() {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
Simplify{' '}
Simplify{" "}
<span className="relative whitespace-normal lg:whitespace-nowrap text-primary">
<svg
aria-hidden="true"
@@ -94,10 +94,8 @@ export function Hero() {
>
<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>{' '}
<span className="relative">Application and Database</span>
</span>{" "}
Deployments
</motion.h1>
<motion.p
@@ -106,9 +104,8 @@ export function Hero() {
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.
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"
@@ -119,21 +116,16 @@ 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',
)
.then(() =>
setIsCopied(true),
)
.catch(() =>
setIsCopied(false),
"curl -sSL https://dokploy.com/install.sh | sh",
)
.then(() => setIsCopied(true))
.catch(() => setIsCopied(false))
}
>
{isCopied ? (
@@ -145,20 +137,14 @@ 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
@@ -214,11 +200,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,8 +18,8 @@ export function NavLink({
href={href}
onClick={() =>
trackGAEvent({
action: 'Nav Link Clicked',
category: 'Navigation',
action: "Nav Link Clicked",
category: "Navigation",
label: href,
})
}
@@ -29,5 +29,5 @@ export function NavLink({
{children}
</Link>
</div>
)
);
}

View File

@@ -1,40 +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.',
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',
"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',
name: "Real-Time Traefik Configuration",
summary:
'Modify Traefik settings on-the-fly via a graphical interface or API.',
"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',
image: "/secondary/traefik.png",
icon: function ReportingIcon() {
return (
<>
@@ -84,12 +84,7 @@ 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"
@@ -97,19 +92,9 @@ 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
@@ -228,63 +213,63 @@ const features: Array<Feature> = [
</g>
</svg>
</>
)
);
},
},
{
name: 'User Permission Management',
name: "User Permission Management",
summary:
'Detailed control over user permissions for accessing and managing projects and services.',
"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',
"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',
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',
"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 />
@@ -294,7 +279,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,
}}
@@ -302,8 +287,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}
@@ -315,7 +300,7 @@ function Feature({
{feature.description}
</p>
</div>
)
);
}
function FeaturesMobile() {
@@ -323,11 +308,7 @@ 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">
@@ -342,7 +323,7 @@ function FeaturesMobile() {
</div>
))}
</div>
)
);
}
function FeaturesDesktop() {
@@ -375,9 +356,8 @@ 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}%)`,
@@ -400,7 +380,7 @@ function FeaturesDesktop() {
</>
)}
</Tab.Group>
)
);
}
export function SecondaryFeatures() {
@@ -416,15 +396,14 @@ 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,16 +1,12 @@
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">
@@ -21,5 +17,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,42 +151,34 @@ 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 (
@@ -200,9 +192,8 @@ export function Testimonials() {
Why Developers Love Dokploy
</h2>
<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.
Think were bragging? Hear from the devs who once doubted toountil
Dokploy made their lives (and deployments) surprisingly easier.
</p>
</div>
@@ -221,5 +212,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,9 +40,7 @@ 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">
@@ -58,12 +56,10 @@ 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>
@@ -71,5 +67,5 @@ export function BlogCard({ post }: BlogCardProps) {
</div>
</div>
</div>
)
);
}

View File

@@ -1,44 +1,43 @@
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 mx-auto max-w-7xl py-10 lg:py-40">
<div className="px-8">
@@ -47,69 +46,61 @@ export function FeaturesSectionDemo() {
</h4>
<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.
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="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('relative overflow-hidden p-4 sm:p-8', className)}>
<div className={cn("relative overflow-hidden p-4 sm:p-8", className)}>
{children}
</div>
)
}
);
};
const FeatureTitle = ({ children }: { children?: React.ReactNode }) => {
return (
<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(
'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',
"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 (
@@ -130,8 +121,8 @@ export const SkeletonOne = () => {
<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 (
@@ -154,17 +145,17 @@ export const SkeletonThree = () => {
</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: {
@@ -177,7 +168,7 @@ export const SkeletonTwo = () => {
rotate: 0,
zIndex: 100,
},
}
};
return (
<div className="relative flex h-full flex-col items-start gap-10 overflow-hidden p-8">
{/* TODO */}
@@ -229,13 +220,13 @@ export const SkeletonTwo = () => {
<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="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,93 +10,93 @@ 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="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-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.
Unlock seamless multi-server deployments, advanced user control, and
flexible database managementall with Dokploys developer-focused
features.
</p>
<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) => (
@@ -104,7 +104,7 @@ export function FirstFeaturesSection() {
))}
</div>
</div>
)
);
}
const Feature = ({
@@ -113,19 +113,18 @@ 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(
'group/feature relative flex flex-col border-neutral-800 py-10 lg:border-r',
"group/feature relative flex flex-col border-neutral-800 py-10 lg:border-r",
(index === 0 || index === 4 || index === 8) &&
'dark:border-neutral-800 lg:border-l',
(index < 4 || index < 8) &&
'dark:border-neutral-800 lg:border-b',
"dark:border-neutral-800 lg:border-l",
(index < 4 || index < 8) && "dark:border-neutral-800 lg:border-b",
)}
>
{index < 4 && (
@@ -134,9 +133,7 @@ const Feature = ({
{index >= 4 && (
<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="relative z-10 mb-4 px-10 text-neutral-400">
{icon}
</div>
<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">
@@ -147,5 +144,5 @@ const Feature = ({
{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,87 +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',
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',
"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',
title: "Docker Compose",
description:
'Native Docker Compose support so you can manage complex applications and services with ease.',
image: '/compose.png',
"Native Docker Compose support so you can manage complex applications and services with ease.",
image: "/compose.png",
},
{
title: 'Multiserver',
title: "Multiserver",
description:
'Deploy applications to multiple servers without the extra effort.',
image: '/remote.png',
"Deploy applications to multiple servers without the extra effort.",
image: "/remote.png",
},
{
title: 'Logs',
title: "Logs",
description:
"Monitor and manage your applications' logs with ease, ensuring efficient troubleshooting and optimal performance.",
image: '/logs.png',
image: "/logs.png",
},
{
title: 'Monitoring',
title: "Monitoring",
description:
"Monitor your systems' performance and health in real time, ensuring continuous and uninterrupted operation.",
image: '/primary/monitoring.png',
image: "/primary/monitoring.png",
},
{
title: 'Backups',
title: "Backups",
description:
'Implement automatic and secure backup solutions to protect your critical data and restore it quickly when necessary.',
image: '/backups.png',
"Implement automatic and secure backup solutions to protect your critical data and restore it quickly when necessary.",
image: "/backups.png",
},
{
title: 'Traefik',
title: "Traefik",
description:
'Manage Traefik via File Editor to configure your own domain names, certificates, and more.',
image: '/traefik.png',
"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
@@ -95,9 +95,8 @@ export function SecondaryFeaturesSections() {
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
@@ -119,12 +118,11 @@ 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"
@@ -132,7 +130,7 @@ export function SecondaryFeaturesSections() {
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
type: 'spring',
type: "spring",
bounce: 0.2,
duration: 0.5,
}}
@@ -142,7 +140,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" />
@@ -151,7 +149,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}
@@ -172,17 +170,11 @@ export function SecondaryFeaturesSections() {
<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,
}}
@@ -196,10 +188,7 @@ export function SecondaryFeaturesSections() {
<span className="h-3 w-3 rounded-full bg-green-400" />
</div>
<div className="h-96 w-full bg-gray-100">
<img
src={feature.image}
alt={feature.title}
/>
<img src={feature.image} alt={feature.title} />
</div>
</div>
</div>
@@ -212,5 +201,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,14 +1,14 @@
'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 (
@@ -18,10 +18,10 @@ export const Sponsors = () => {
Sponsors
</h3>
<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.
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">
@@ -29,13 +29,12 @@ 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:
'm-0 w-fit !rounded-full bg-transparent !p-0',
variant: "secondary",
size: "sm",
className: "m-0 w-fit !rounded-full bg-transparent !p-0",
})}
>
<PlusCircleIcon className="size-10 text-muted-foreground transition-colors hover:text-primary" />
@@ -49,5 +48,5 @@ export const Sponsors = () => {
<Ripple />
</div>
</div>
)
}
);
};

View File

@@ -1,39 +1,39 @@
'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="flex flex-col gap-10 px-4 py-20 lg:py-40 ">
@@ -42,9 +42,8 @@ export function StatsSection() {
Stats You Didn't Ask For (But Secretly Love to See)
</h2>
<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?
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="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">
@@ -60,23 +59,23 @@ export function StatsSection() {
{feature.icon}
</p>
<p className="relative z-20 mt-4 text-base font-normal text-neutral-400">
{typeof feature.description === 'function'
{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: (
@@ -91,8 +90,8 @@ const grid = [
),
},
{
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"
@@ -112,7 +111,7 @@ const grid = [
),
},
{
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: (
@@ -122,7 +121,7 @@ const grid = [
),
},
{
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: (
@@ -131,14 +130,14 @@ const grid = [
</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],
@@ -146,7 +145,7 @@ 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 from-zinc-900/30 to-zinc-900/30 opacity-100 [mask-image:radial-gradient(farthest-side_at_top,white,transparent)]">
@@ -160,11 +159,11 @@ export const Grid = ({
/>
</div>
</div>
)
}
);
};
export function GridPattern({ width, height, x, y, squares, ...props }: any) {
const patternId = useId()
const patternId = useId();
return (
<svg aria-hidden="true" {...props}>
@@ -201,5 +200,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(
'group flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
"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 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]',
"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] [border-radius:inherit] ![mask-composite:subtract] [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,60 +1,59 @@
'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,84 +1,84 @@
'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="group relative cursor-pointer"
onClick={() => setIsVideoOpen(true)}
@@ -94,13 +94,14 @@ export default function HeroVideoDialog({
<div className="flex size-28 items-center justify-center rounded-full bg-primary/10 backdrop-blur-md">
<div
className={
'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]'
"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 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,7 +120,7 @@ export default function HeroVideoDialog({
<motion.div
{...selectedAnimation}
transition={{
type: 'spring',
type: "spring",
damping: 30,
stiffness: 300,
}}
@@ -142,5 +143,5 @@ export default function HeroVideoDialog({
)}
</AnimatePresence>
</div>
)
);
}

View File

@@ -1,74 +1,76 @@
'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 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',
"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(
'z-10 w-auto rounded-[inherit] bg-black px-4 py-2 text-white',
"z-10 w-auto rounded-[inherit] bg-black px-4 py-2 text-white",
className,
)}
>
@@ -76,13 +78,13 @@ export function HoverBorderGradient({
</div>
<motion.div
className={cn(
'absolute inset-0 z-0 flex-none overflow-hidden 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={{
@@ -90,9 +92,9 @@ export function HoverBorderGradient({
? [movingMap[direction], highlight]
: movingMap[direction],
}}
transition={{ ease: 'linear', duration: duration ?? 1 }}
transition={{ ease: "linear", duration: duration ?? 1 }}
/>
<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,28 +26,26 @@ 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,
@@ -55,17 +53,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,20 +36,16 @@ 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 tracking-wider text-white',
"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,83 +33,87 @@ 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
@@ -120,12 +124,11 @@ 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
@@ -137,22 +140,21 @@ const Ripple = React.memo(function Ripple({
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
@@ -161,63 +163,45 @@ const Ripple = React.memo(function Ripple({
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 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>
@@ -231,7 +215,7 @@ const Ripple = React.memo(function Ripple({
</Tooltip>
</TooltipProvider>
</div>
)
);
})}
</div>
)}
@@ -239,53 +223,34 @@ const Ripple = React.memo(function Ripple({
{i === 1 && (
<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>
@@ -299,7 +264,7 @@ const Ripple = React.memo(function Ripple({
</Tooltip>
</TooltipProvider>
</div>
)
);
})}
</div>
)}
@@ -307,52 +272,34 @@ const Ripple = React.memo(function Ripple({
{i === 2 && (
<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>
@@ -366,60 +313,41 @@ const Ripple = React.memo(function Ripple({
</Tooltip>
</TooltipProvider>
</div>
)
);
})}
</div>
)}
{i === 3 && (
<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>
@@ -433,7 +361,7 @@ const Ripple = React.memo(function Ripple({
</Tooltip>
</TooltipProvider>
</div>
)
);
})}
</div>
)}
@@ -441,53 +369,34 @@ const Ripple = React.memo(function Ripple({
{i === 4 && (
<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>
@@ -499,18 +408,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,31 +20,29 @@ 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

@@ -10,13 +10,13 @@ const nextConfig = {
},
images: {
domains: [
'static.ghost.org',
'testing-ghost-8423be-31-220-108-27.traefik.me',
'images.unsplash.com',
'www.gravatar.com',
'cms.dokploy.com',
"static.ghost.org",
"testing-ghost-8423be-31-220-108-27.traefik.me",
"images.unsplash.com",
"www.gravatar.com",
"cms.dokploy.com",
],
},
}
};
module.exports = nextConfig
module.exports = nextConfig;

View File

@@ -1,153 +1,150 @@
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;