refactor: standardize code formatting and improve readability

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared"; import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
import { import {
Github, Github,
GlobeIcon, GlobeIcon,
HeartIcon, HeartIcon,
Rss, Rss,
LogIn, LogIn,
UserPlus, UserPlus,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
/** /**
@@ -17,106 +17,106 @@ import Link from "next/link";
*/ */
export const Logo = () => { export const Logo = () => {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 559 446" viewBox="0 0 559 446"
className="!size-8 lg:!size-10" className="!size-8 lg:!size-10"
> >
<path <path
className="fill-primary stroke-primary" className="fill-primary stroke-primary"
d="M390 56v12c.1 2.3.5 4 1 6a73 73 0 0 0 12 24c2 2.3 5.7 4 7 7 4 3.4 9.6 6.8 14 9 1.7.6 5.7 1.1 7 2 1.9 1.3 2.9 2.3 0 4v1c-.6 1.8-1.9 3.5-3 5q-3 4-7 7c-4.3 3.2-9.5 6.8-15 7h-1q-2 1.6-5 2h-4c-5.2.7-12.9 2.2-18 0h-6c-1.6 0-3-.8-4-1h-3a17 17 0 0 1-6-2h-1c-2.5-.1-4-1.2-6-2l-4-1c-8.4-2-20.3-6.6-27-12h-1c-4.6-1-9.5-4.3-13.7-6.3s-10.5-3-13.3-6.7h-1c-4-1-8.9-3.5-12-6h-1c-6.8-1.6-13.6-6-20-9-6.5-2.8-14.6-5.7-20-10h-1c-7-1.2-15.4-4-22-6h-97c-5.3 4.3-13.7 4.3-18.7 10.3S90.8 101 88 108c-.4 1.5-.8 2.3-1 4-.2 1.6-.8 4-1 5v51c.2 1.2.8 3.2 1 5 .2 2 .5 3.2 1 5a79 79 0 0 0 6 12c.8.7 1.4 2.2 2 3 1.8 2 4.9 3.4 6 6 9.5 8.3 23.5 10.3 33 18h1c5.1 1.2 12 4.8 16 8h1c4 1 8.9 3.5 12 6h1q4.6 1.2 8 4h1c2 .1 2.6 1.3 4 2 1.6.8 2.7.7 4 2h1q2.5.3 4 2h1c3 .7 6.7 2 9 4h1c4.7.8 13.4 3.1 17 6h1c2.5.1 4 1.3 6 2 1.8.4 3 .8 5 1q3 .4 5 1c1.6-.2 2 0 3 1h1q2.5-.5 4 1h1q2.5-.5 4 1h1c2.2-.2 4.5-.3 6 1h1q4-.4 7 1h45c1.2-.2 3.1-1 5-1h6c1.5-.6 2.9-1.3 5-1h1q1.5-1.4 4-1h1q1.5-1.4 4-1h1c2.4-1.3 5-1.6 8-2l5-1c2-.7 3.6-1.6 6-2 4-.7 7.2-1.7 11-3 2.3-1 4.2-2.5 7-3h1q1.5-1.7 4-2h1c1.9-1.5 3.9-2 6-3q2.9-1.6 6-3a95 95 0 0 0 11-5c4.4-2.8 8.9-6 14-8 0 0 .6.2 1 0 1.8-2.8 7-4.8 10-6 0 0 .6.2 1 0 1.5-2.4 5.3-4 8-5 0 0 .6.2 1 0 1.5-2.4 5.3-4 8-5 0 0 .6.2 1 0 1.3-2 3.8-3.1 6-4 0 0 .6.2 1 0 2-3 7.7-5.6 11-7l5-2c6.3-3.8 11.8-9.6 18-14v-1c0-1.9-.4-4.2 0-6-1-4.5-3.9-5.5-7-8h-1c-1.2 0-2.8-.2-4 0-8.9 1.7-16.5 11.3-25.2 14.8-8.8 3.4-16.9 10.7-25.8 14.2h-1c-10.9 10.6-29.2 16-42.7 23.3S343.7 234.6 328 235h-1q-1.5 1.4-4 1h-1q-1.5 1.4-4 1h-1c-1.5 1.3-3.9 1.2-6 1h-1c-1.7 1.3-4.6 1.2-7 1-1 .2-2.4 1-4 1h-5c-6.6 0-13.4.4-20 0-1.9-.1-2.7.3-4-1h-8c-2.8-.2-5.7-1.3-8-2h-2q-5.7.4-10-2h-1q-4.5 0-8-2h-1a10 10 0 0 1-6-2h-1c-5.9-.2-12-3.8-17-6l-4-1c-1.7-.5-2.8-.7-4-2h-1q-2.5-.2-4-2h-1q-3.4-.9-6-3h-1c-3.5-.8-7.3-2.9-10-5h-1c-1.7 0-2.2-.7-3-2h-1c-11.6-2.7-23.2-11.5-34.2-15.8-11-4.2-25.9-9.2-29.8-21.2h4c16.2 0 32.8-1 49 0 1.7.1 3 .8 4 1 2.1.4 3.4-.5 5 1h1c3.6.1 8.4 1.8 11 4h1a45 45 0 0 1 18 8h1q4.6 1.2 8 4h1c4.2 1 8.3 3.4 12 5q3.4 1.2 7 2c5.7 1.3 13 2.3 18 5h1c3.7-.2 7 1.1 10 2h9c1.6 0 3 .8 4 1h32c2.2-1.6 6-1 9-1h1a63 63 0 0 1 22-4 22 22 0 0 1 8-2c1.7-1.4 3.7-1.6 6-2a81 81 0 0 0 12-3c2.3-1 4.2-2.5 7-3h1q1.5-1.7 4-2h1c1.9-1.5 3.6-2.2 6-3l3-1c4.1-2.3 8.4-5.2 13-7 0 0 .6.2 1 0 1.5-2.4 6.3-5 9-6 0 0 .6.2 1 0 5.3-8.1 17.6-12.5 24.8-20.2C439.9 144 445 133 452 126v-1a12 12 0 0 1 2-5c2.1-2.2 8.9-1 12-1q2 .2 4 0c1-.2 2.3-1.2 4-1h1q2.1-1.5 5-2h1q2.1-1.9 5-3s.6.2 1 0c9-9.3 18-15.4 23-28 1.1-2.8 3.5-6.4 4-9 .2-1 .2-3 0-4-1.5-6-12.3-2.4-15.7 2.3S484.7 80 479 80h-7c-7.8 4.3-19.3 5.7-23 16a37 37 0 0 0-22-24c-1.5-.5-2.5-.7-4-1-2.1-.5-3.6-.2-5-2h-1a22 22 0 0 1-12-8c-2-2.9-3.4-6.5-6-9h-1c-3.9-.6-6.1 1-8 4m-181 45h1c2.2-.2 4.5-.3 6 1h1q2.5-.5 4 1h1a33 33 0 0 1 17 7h1c4.4 1 8.2 4.1 12 6 2.1 1 4.1 1.5 6 3h1c4 1 8.9 3.5 12 6h1c4 1 8.9 3.5 12 6h1c4 1 8.9 3.5 12 6h1a61 61 0 0 1 21 10h1c3.5.8 7.3 2.9 10 5h1c6.1 1.4 12.3 5 18 7 1.8.4 3 .8 5 1 1.8.2 3.7.8 5 1q2.5-.5 4 1h6c2.5 0 4 .3 6 1h3q-.7 2.1-3 2a46 46 0 0 1-16 7l-10 3c-2 .8-3.4 1.9-6 2h-1c-2.6 2.1-7.5 3-11 3h-1c-3.1 2.5-10.7 3.5-15 3h-1c-1.5 1.3-3.9 1.2-6 1-1 .2-2.4 1-4 1h-11c-3.8.4-8.3.4-12 0h-9c-2.3 0-4.3-.7-6-1h-3c-1.8 0-2.9-.7-4-1-3.5-.8-7-.7-10-2h-1c-4.1-.7-9.8-1.4-13-4h-1q-4-.6-7-3h-1q-2.5-.2-4-2h-1q-3.4-.9-6-3h-1c-7.2-1.7-13.3-5.9-20.2-8.8-7-2.8-16.2-4.3-22.8-7.2h-11c-14 0-28.9.3-42-1-2.3 0-4.8.3-7 0a6 6 0 0 1-5-5c-1.8-4.8-.4-10.4 0-15 0-4.3-.4-8.7 0-13 .2-3.2 2.2-7.3 4-10q2-3 5-5c2.1-2 5.4-2.3 8-3 15.6-3.9 36.3-1 53-1 5.2 0 12-.5 17 0s12.2-1.8 16 1Z" d="M390 56v12c.1 2.3.5 4 1 6a73 73 0 0 0 12 24c2 2.3 5.7 4 7 7 4 3.4 9.6 6.8 14 9 1.7.6 5.7 1.1 7 2 1.9 1.3 2.9 2.3 0 4v1c-.6 1.8-1.9 3.5-3 5q-3 4-7 7c-4.3 3.2-9.5 6.8-15 7h-1q-2 1.6-5 2h-4c-5.2.7-12.9 2.2-18 0h-6c-1.6 0-3-.8-4-1h-3a17 17 0 0 1-6-2h-1c-2.5-.1-4-1.2-6-2l-4-1c-8.4-2-20.3-6.6-27-12h-1c-4.6-1-9.5-4.3-13.7-6.3s-10.5-3-13.3-6.7h-1c-4-1-8.9-3.5-12-6h-1c-6.8-1.6-13.6-6-20-9-6.5-2.8-14.6-5.7-20-10h-1c-7-1.2-15.4-4-22-6h-97c-5.3 4.3-13.7 4.3-18.7 10.3S90.8 101 88 108c-.4 1.5-.8 2.3-1 4-.2 1.6-.8 4-1 5v51c.2 1.2.8 3.2 1 5 .2 2 .5 3.2 1 5a79 79 0 0 0 6 12c.8.7 1.4 2.2 2 3 1.8 2 4.9 3.4 6 6 9.5 8.3 23.5 10.3 33 18h1c5.1 1.2 12 4.8 16 8h1c4 1 8.9 3.5 12 6h1q4.6 1.2 8 4h1c2 .1 2.6 1.3 4 2 1.6.8 2.7.7 4 2h1q2.5.3 4 2h1c3 .7 6.7 2 9 4h1c4.7.8 13.4 3.1 17 6h1c2.5.1 4 1.3 6 2 1.8.4 3 .8 5 1q3 .4 5 1c1.6-.2 2 0 3 1h1q2.5-.5 4 1h1q2.5-.5 4 1h1c2.2-.2 4.5-.3 6 1h1q4-.4 7 1h45c1.2-.2 3.1-1 5-1h6c1.5-.6 2.9-1.3 5-1h1q1.5-1.4 4-1h1q1.5-1.4 4-1h1c2.4-1.3 5-1.6 8-2l5-1c2-.7 3.6-1.6 6-2 4-.7 7.2-1.7 11-3 2.3-1 4.2-2.5 7-3h1q1.5-1.7 4-2h1c1.9-1.5 3.9-2 6-3q2.9-1.6 6-3a95 95 0 0 0 11-5c4.4-2.8 8.9-6 14-8 0 0 .6.2 1 0 1.8-2.8 7-4.8 10-6 0 0 .6.2 1 0 1.5-2.4 5.3-4 8-5 0 0 .6.2 1 0 1.5-2.4 5.3-4 8-5 0 0 .6.2 1 0 1.3-2 3.8-3.1 6-4 0 0 .6.2 1 0 2-3 7.7-5.6 11-7l5-2c6.3-3.8 11.8-9.6 18-14v-1c0-1.9-.4-4.2 0-6-1-4.5-3.9-5.5-7-8h-1c-1.2 0-2.8-.2-4 0-8.9 1.7-16.5 11.3-25.2 14.8-8.8 3.4-16.9 10.7-25.8 14.2h-1c-10.9 10.6-29.2 16-42.7 23.3S343.7 234.6 328 235h-1q-1.5 1.4-4 1h-1q-1.5 1.4-4 1h-1c-1.5 1.3-3.9 1.2-6 1h-1c-1.7 1.3-4.6 1.2-7 1-1 .2-2.4 1-4 1h-5c-6.6 0-13.4.4-20 0-1.9-.1-2.7.3-4-1h-8c-2.8-.2-5.7-1.3-8-2h-2q-5.7.4-10-2h-1q-4.5 0-8-2h-1a10 10 0 0 1-6-2h-1c-5.9-.2-12-3.8-17-6l-4-1c-1.7-.5-2.8-.7-4-2h-1q-2.5-.2-4-2h-1q-3.4-.9-6-3h-1c-3.5-.8-7.3-2.9-10-5h-1c-1.7 0-2.2-.7-3-2h-1c-11.6-2.7-23.2-11.5-34.2-15.8-11-4.2-25.9-9.2-29.8-21.2h4c16.2 0 32.8-1 49 0 1.7.1 3 .8 4 1 2.1.4 3.4-.5 5 1h1c3.6.1 8.4 1.8 11 4h1a45 45 0 0 1 18 8h1q4.6 1.2 8 4h1c4.2 1 8.3 3.4 12 5q3.4 1.2 7 2c5.7 1.3 13 2.3 18 5h1c3.7-.2 7 1.1 10 2h9c1.6 0 3 .8 4 1h32c2.2-1.6 6-1 9-1h1a63 63 0 0 1 22-4 22 22 0 0 1 8-2c1.7-1.4 3.7-1.6 6-2a81 81 0 0 0 12-3c2.3-1 4.2-2.5 7-3h1q1.5-1.7 4-2h1c1.9-1.5 3.6-2.2 6-3l3-1c4.1-2.3 8.4-5.2 13-7 0 0 .6.2 1 0 1.5-2.4 6.3-5 9-6 0 0 .6.2 1 0 5.3-8.1 17.6-12.5 24.8-20.2C439.9 144 445 133 452 126v-1a12 12 0 0 1 2-5c2.1-2.2 8.9-1 12-1q2 .2 4 0c1-.2 2.3-1.2 4-1h1q2.1-1.5 5-2h1q2.1-1.9 5-3s.6.2 1 0c9-9.3 18-15.4 23-28 1.1-2.8 3.5-6.4 4-9 .2-1 .2-3 0-4-1.5-6-12.3-2.4-15.7 2.3S484.7 80 479 80h-7c-7.8 4.3-19.3 5.7-23 16a37 37 0 0 0-22-24c-1.5-.5-2.5-.7-4-1-2.1-.5-3.6-.2-5-2h-1a22 22 0 0 1-12-8c-2-2.9-3.4-6.5-6-9h-1c-3.9-.6-6.1 1-8 4m-181 45h1c2.2-.2 4.5-.3 6 1h1q2.5-.5 4 1h1a33 33 0 0 1 17 7h1c4.4 1 8.2 4.1 12 6 2.1 1 4.1 1.5 6 3h1c4 1 8.9 3.5 12 6h1c4 1 8.9 3.5 12 6h1c4 1 8.9 3.5 12 6h1a61 61 0 0 1 21 10h1c3.5.8 7.3 2.9 10 5h1c6.1 1.4 12.3 5 18 7 1.8.4 3 .8 5 1 1.8.2 3.7.8 5 1q2.5-.5 4 1h6c2.5 0 4 .3 6 1h3q-.7 2.1-3 2a46 46 0 0 1-16 7l-10 3c-2 .8-3.4 1.9-6 2h-1c-2.6 2.1-7.5 3-11 3h-1c-3.1 2.5-10.7 3.5-15 3h-1c-1.5 1.3-3.9 1.2-6 1-1 .2-2.4 1-4 1h-11c-3.8.4-8.3.4-12 0h-9c-2.3 0-4.3-.7-6-1h-3c-1.8 0-2.9-.7-4-1-3.5-.8-7-.7-10-2h-1c-4.1-.7-9.8-1.4-13-4h-1q-4-.6-7-3h-1q-2.5-.2-4-2h-1q-3.4-.9-6-3h-1c-7.2-1.7-13.3-5.9-20.2-8.8-7-2.8-16.2-4.3-22.8-7.2h-11c-14 0-28.9.3-42-1-2.3 0-4.8.3-7 0a6 6 0 0 1-5-5c-1.8-4.8-.4-10.4 0-15 0-4.3-.4-8.7 0-13 .2-3.2 2.2-7.3 4-10q2-3 5-5c2.1-2 5.4-2.3 8-3 15.6-3.9 36.3-1 53-1 5.2 0 12-.5 17 0s12.2-1.8 16 1Z"
/> />
<path <path
className="fill-primary stroke-primary" className="fill-primary stroke-primary"
d="M162 132v1c1.8 2.9 4.5 5.3 8 6 .3-.2 3.7-.2 4 0 7-1.4 9.2-8.8 7-15v-1a14 14 0 0 0-7-4c-.3.2-3.7.2-4 0-6.5 1.3-8.6 6.8-8 13Z" d="M162 132v1c1.8 2.9 4.5 5.3 8 6 .3-.2 3.7-.2 4 0 7-1.4 9.2-8.8 7-15v-1a14 14 0 0 0-7-4c-.3.2-3.7.2-4 0-6.5 1.3-8.6 6.8-8 13Z"
/> />
<path <path
className="fill-primary stroke-primary" className="fill-primary stroke-primary"
d="M465 211h-1c-18.2 14.6-41.2 24.6-60 39-19 14.2-42.7 29.3-66 34l-4 1c-2.4 1-4 2-7 2h-1q-3.5 2-8 2h-1c-1.3 1.2-3 1.1-5 1h-2q-2.6 1.1-6 1h-2c-3 1.2-6.5 1-10 1-6.3.6-13.8.6-20 0-3.4 0-8.4.9-11-1h-1c-2.2.2-4.5.3-6-1h-1c-2 .2-3.7.2-5-1h-1c-7.6.5-16.5-3.4-23-6l-4-1a129 129 0 0 1-36.2-15.8c-10.4-6.6-23.2-12.8-32.5-20.5-9.2-7.7-23.8-12.8-30.3-22.7h-1c-2.3-1.4-4.5-2.7-6-5h-1c-4-2.5-8.5-5.2-12-8h-9a9 9 0 0 0-6 7c.3 3.3 0 6.7 0 10v9c.2 1.6 1 3.8 1 6v3c.2 1 1.2 2.2 1 4v1c1.2 1.2.8 2.2 1 4 .8 6.7 3 12.6 5 19 1.7 4.3 4.2 9.1 5 14v1q1.8 1.5 2 4v1a36 36 0 0 1 5 10c.7 2 1 3 2 5 8 12.7 15.7 25.5 25.8 37.3 10 11.7 20.8 20.6 32.4 30.4 11.7 9.9 28.3 14 39.8 23.3h1q2.5.3 4 2h1c2.8.4 4.8 2 7 3l7 2c5.7 1.3 13 2.3 18 5h1c2.1-.3 3.6.8 5 1h3c2.8.2 5.8 1 8 2h8c2.1 0 4.6.8 6 1h21c1.2-.2 3.2-1 5-1h9c3.3-1 7-2.4 11-2h1c2.7-2.2 7.4-2.4 11-3a55 55 0 0 0 8-2c6.5-2.6 13.9-6.3 21-8h1c8.5-6.8 20.6-9.7 29.2-16.8 8.7-7 18.3-12.8 26.8-20.2 4.4-3.8 9-9 13-13 14.8-14.8 20.7-34.6 33-50v-1q.9-3.4 3-6v-1q.3-2.5 2-4v-1c.5-3.3 2-8.6 4-11v-1q0-3.5 2-6v-1c1.1-6.7 2.4-15 5-21v-1c-.2-2-.2-3.7 1-5v-8c0-5.3-.5-10.8 0-16a14 14 0 0 0-4-6c-1-.5-1.1-.4-2-1h-6q-2.1 1.5-5 2m-6 38c-2.1 13.4-21.2 20.3-31 30-10 9.5-23.7 19-35 27-11.5 8-25.1 19.7-39 23h-1a22 22 0 0 1-10 4h-1a25 25 0 0 1-12 4h-1q-3.5 2-8 2h-1c-1.1 1.1-2.3 1-4 1h-2c-1.2.4-2.2 1-4 1h-2c-1.8.7-3.6 1.3-6 1h-1c-1.2 1.2-2.3 1-4 1h-5c-5.7.6-12.3.8-18 0h-4c-1.9 0-2.7-.6-4-1h-6c-1.9 0-2.7.3-4-1h-1q-2.5.5-4-1h-1c-8.1.5-16.8-3.6-24.2-5.8S210 329.8 204 325h-1c-12.8-5-27.1-15.6-37.7-24.3S138.8 284.2 131 273c-.3-.2-1 0-1 0-5.7-4.4-16.6-10-19-17-.9-2.6-1-5.4-2-8-.8-2.2-2.5-5-2-8a667 667 0 0 0 88 56h1q3.4.9 6 3h1c2.8.4 4.8 2 7 3q5 1.8 10 3l6 2q2.9.6 6 1 3 .4 5 1c1.6-.2 2 0 3 1h1c2-.2 3.7-.2 5 1h1c2.2-.3 3.4.4 5 1h8c1.6 0 3 .9 4 1h40c1.8-1.3 4.6-1.2 7-1h1c1.2-1.2 3.2-1.2 5-1h1c1.2-1.2 3.2-1.2 5-1h1c1.1-1.1 2.3-1 4-1h2c3.5-1.7 6.9-2.3 11-3l4-1c3.4-1.4 7.1-3 11-4 1.5-.4 2.5-.5 4-1 1.4-.7 2-1.9 4-2h1q2.6-2.1 6-3h1c2.5-2 6-3.8 9-5l3-1c1.4-.9 2-2.5 4-3h1q1.4-2.2 4-3h1c7.3-7.7 19-13.2 27.7-19.3 8.8-6.1 18.2-15 28.3-18.7.4-.2 1 0 1 0q3.8-3.9 9-6c1.3 2.5-.5 6.7-1 10m-20 55c-.2.4 0 1 0 1-3.4 9.6-12.7 19-19 27a88 88 0 0 1-12 12 214 214 0 0 1-26.7 20.3c-9.5 5.8-20 14.8-31.3 16.7h-1a22 22 0 0 1-10 4h-1c-3.2 2.6-8.9 3.3-13 4h-1q-1.5 1.4-4 1h-1q-1.5 1.4-4 1h-1c-4.9 2.3-10.5 1-16 2-1 .2-2.5 1-4 1-6.2.4-12.8.3-19 0-1.8 0-3.8-.8-5-1h-4c-1.6 0-3-.9-4-1h-4c-3.9-.3-8.8-1.3-12-3h-1c-3.3-.5-7.5-1-10-3h-1c-3.6-.1-8.4-1.8-11-4h-1c-3.9-.6-8-2.6-11-5h-1c-16.1-3.8-32.2-18.9-45-29a200 200 0 0 1-40-51c17.7 11.5 35 25.5 52 38h1c4 1.6 12.8 5.4 15 9h1c4.6 1 10.4 4.1 14 7h1q2.5.3 4 2h1c3.3.5 8.6 2 11 4h1q3.5 0 6 2h1q2.5-.5 4 1h1q2.5-.5 4 1h1c3.8-.2 7.9 1 11 2h9c1.6 0 3 .8 4 1h32c1.2-.2 3.2-1 5-1h8a139 139 0 0 1 20-4l5-1c2-.7 3.7-1.5 6-2l4-1c1.5-.6 3-1.7 5-2h1q3-2.4 7-3h1q2.6-2.1 6-3h1c11.7-9.4 27.6-14.6 39-25 11.6-10.3 25-18.5 37-28a15 15 0 0 1-5 10Z" 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> </svg>
); );
}; };
export const baseOptions: BaseLayoutProps = { export const baseOptions: BaseLayoutProps = {
nav: { nav: {
// title: "Dokploy", // title: "Dokploy",
children: ( children: (
<Link href="/docs/core" className="flex items-center gap-2"> <Link href="/docs/core" className="flex items-center gap-2">
<Logo /> <Logo />
<span className="text-foreground font-semibold">Dokploy</span> <span className="text-foreground font-semibold">Dokploy</span>
</Link> </Link>
), ),
}, },
links: [ links: [
{ {
text: "Login", text: "Login",
url: "https://app.dokploy.com/", url: "https://app.dokploy.com/",
active: "nested-url", active: "nested-url",
icon: <LogIn />, icon: <LogIn />,
}, },
{ {
text: "Sign Up", text: "Sign Up",
url: "https://app.dokploy.com/register", url: "https://app.dokploy.com/register",
active: "nested-url", active: "nested-url",
icon: <UserPlus />, icon: <UserPlus />,
}, },
{ {
text: "Website", text: "Website",
url: "https://dokploy.com", url: "https://dokploy.com",
active: "nested-url", active: "nested-url",
icon: <GlobeIcon />, icon: <GlobeIcon />,
}, },
{ {
text: "Discord", text: "Discord",
url: "https://discord.com/invite/2tBnJ3jDJc", url: "https://discord.com/invite/2tBnJ3jDJc",
active: "nested-url", active: "nested-url",
icon: ( icon: (
<> <>
<svg <svg
role="img" role="img"
className="size-6 " className="size-6 "
fill="currentColor" fill="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" 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" /> <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> </svg>
</> </>
), ),
}, },
{ {
text: "Support", text: "Support",
url: "https://opencollective.com/dokploy", url: "https://opencollective.com/dokploy",
active: "nested-url", active: "nested-url",
icon: ( icon: (
<> <>
<HeartIcon fill="currentColor" /> <HeartIcon fill="currentColor" />
</> </>
), ),
}, },
{ {
text: "Github", text: "Github",
url: "https://github.com/dokploy/dokploy", url: "https://github.com/dokploy/dokploy",
active: "nested-url", active: "nested-url",
icon: ( icon: (
<> <>
<Github fill="currentColor" /> <Github fill="currentColor" />
</> </>
), ),
}, },
{ {
text: "Blog", text: "Blog",
url: "https://dokploy.com/blog", url: "https://dokploy.com/blog",
active: "nested-url", active: "nested-url",
icon: ( icon: (
<> <>
<Rss /> <Rss />
</> </>
), ),
}, },
], ],
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,35 +1,34 @@
import { getPost } from "@/lib/ghost"; import { getPost } from '@/lib/ghost'
import { generateOGImage } from "@/lib/og-image"; import { generateOGImage } from '@/lib/og-image'
import type { NextRequest } from "next/server"; import type { NextRequest } from 'next/server'
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url)
const slug = searchParams.get("slug"); const slug = searchParams.get('slug')
console.log("Generating OG image for slug:", slug); console.log('Generating OG image for slug:', slug)
if (!slug) { if (!slug) {
console.error("Missing slug parameter"); console.error('Missing slug parameter')
return new Response("Missing slug parameter", { status: 400 }); return new Response('Missing slug parameter', { status: 400 })
} }
const post = await getPost(slug); const post = await getPost(slug)
if (!post) { if (!post) {
console.error("Post not found for slug:", slug); console.error('Post not found for slug:', slug)
return new Response("Post not found", { status: 404 }); return new Response('Post not found', { status: 404 })
} }
const formattedDate = new Date(post.published_at).toLocaleDateString( const formattedDate = new Date(post.published_at).toLocaleDateString(
"en-US", 'en-US',
{ {
year: "numeric", year: 'numeric',
month: "long", month: 'long',
day: "numeric", day: 'numeric',
}, },
); )
const ogImage = await generateOGImage({ const ogImage = await generateOGImage({
title: post.title, title: post.title,
@@ -41,16 +40,16 @@ export async function GET(request: NextRequest) {
: undefined, : undefined,
date: formattedDate, date: formattedDate,
readingTime: post.reading_time, readingTime: post.reading_time,
}); })
return new Response(ogImage, { return new Response(ogImage, {
headers: { headers: {
"Content-Type": "image/png", 'Content-Type': 'image/png',
"Cache-Control": "public, max-age=31536000, immutable", 'Cache-Control': 'public, max-age=31536000, immutable',
}, },
}); })
} catch (error) { } catch (error) {
console.error("Error generating OG image:", error); console.error('Error generating OG image:', error)
return new Response(`Error generating image: ${error}`, { status: 500 }); 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 { CopyButton } from '@/components/ui/copy-button'
import * as babel from "prettier/plugins/babel"; import * as babel from 'prettier/plugins/babel'
import * as estree from "prettier/plugins/estree"; import * as estree from 'prettier/plugins/estree'
import * as yaml from "prettier/plugins/yaml"; import * as yaml from 'prettier/plugins/yaml'
import * as prettier from "prettier/standalone"; import * as prettier from 'prettier/standalone'
import { type JSX, useLayoutEffect, useState } from "react"; import { type JSX, useLayoutEffect, useState } from 'react'
import type { BundledLanguage } from "shiki/bundle/web"; import type { BundledLanguage } from 'shiki/bundle/web'
import { highlight } from "./shared"; import { highlight } from './shared'
interface CodeBlockProps { interface CodeBlockProps {
code: string; code: string
lang: BundledLanguage; lang: BundledLanguage
initial?: JSX.Element; initial?: JSX.Element
} }
async function formatCode(code: string, lang: string) { async function formatCode(code: string, lang: string) {
try { try {
let parser: string; let parser: string
let plugins = [] as any[]; let plugins = [] as any[]
switch (lang.toLowerCase()) { switch (lang.toLowerCase()) {
case "yaml": case 'yaml':
case "yml": case 'yml':
parser = "yaml"; parser = 'yaml'
plugins = [yaml]; plugins = [yaml]
break; break
case "javascript": case 'javascript':
case "typescript": case 'typescript':
case "jsx": case 'jsx':
case "tsx": case 'tsx':
parser = "babel-ts"; parser = 'babel-ts'
plugins = [babel, estree]; plugins = [babel, estree]
break; break
default: default:
return code; return code
} }
const formatted = await prettier.format(code, { const formatted = await prettier.format(code, {
parser, parser,
@@ -43,50 +43,50 @@ async function formatCode(code: string, lang: string) {
tabWidth: 2, tabWidth: 2,
useTabs: false, useTabs: false,
printWidth: 120, printWidth: 120,
}); })
return formatted; return formatted
} catch (error) { } catch (error) {
console.error("Error formatting code:", error); console.error('Error formatting code:', error)
return code; return code
} }
} }
export function CodeBlock({ code, lang, initial }: CodeBlockProps) { export function CodeBlock({ code, lang, initial }: CodeBlockProps) {
const [nodes, setNodes] = useState<JSX.Element | undefined>(initial); const [nodes, setNodes] = useState<JSX.Element | undefined>(initial)
const [formattedCode, setFormattedCode] = useState(code); const [formattedCode, setFormattedCode] = useState(code)
useLayoutEffect(() => { useLayoutEffect(() => {
async function formatAndHighlight() { async function formatAndHighlight() {
try { try {
const formatted = await formatCode(code, lang); const formatted = await formatCode(code, lang)
setFormattedCode(formatted); setFormattedCode(formatted)
const highlighted = await highlight(formatted, lang); const highlighted = await highlight(formatted, lang)
setNodes(highlighted); setNodes(highlighted)
} catch (error) { } catch (error) {
const highlighted = await highlight(code, lang); const highlighted = await highlight(code, lang)
setNodes(highlighted); setNodes(highlighted)
} }
} }
void formatAndHighlight(); void formatAndHighlight()
}, [code, lang]); }, [code, lang])
if (!nodes) { if (!nodes) {
return ( return (
<div className="group relative"> <div className="group relative">
<div className="text-sm p-4 rounded-lg bg-[#18191F] overflow-auto animate-pulse"> <div className="animate-pulse overflow-auto rounded-lg bg-[#18191F] p-4 text-sm">
<div className="h-4 bg-gray-700 rounded w-3/4 mb-2" /> <div className="mb-2 h-4 w-3/4 rounded bg-gray-700" />
<div className="h-4 bg-gray-700 rounded w-1/2" /> <div className="h-4 w-1/2 rounded bg-gray-700" />
</div> </div>
</div> </div>
); )
} }
return ( return (
<div className="group relative"> <div className="group relative">
<CopyButton text={formattedCode} /> <CopyButton text={formattedCode} />
<div className="text-sm p-4 rounded-lg bg-[#18191F] overflow-auto"> <div className="overflow-auto rounded-lg bg-[#18191F] p-4 text-sm">
{nodes} {nodes}
</div> </div>
</div> </div>
); )
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -1,13 +1,12 @@
import type { Metadata } from "next"; import type { Metadata } from 'next'
import type { ReactNode } from "react"; import type { ReactNode } from 'react'
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Contact Us", title: 'Contact Us',
description: description:
"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.",
};
export default function ContactLayout({ children }: { children: ReactNode }) {
return <>{children}</>;
} }
export default function ContactLayout({ children }: { children: ReactNode }) {
return <>{children}</>
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -1,13 +1,16 @@
import clsx from "clsx"; import clsx from 'clsx'
export function Container({ export function Container({
className, className,
...props ...props
}: React.ComponentPropsWithoutRef<"div">) { }: React.ComponentPropsWithoutRef<'div'>) {
return ( return (
<div <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} {...props}
/> />
); )
} }

View File

@@ -3,79 +3,80 @@ import {
AccordionContent, AccordionContent,
AccordionItem, AccordionItem,
AccordionTrigger, AccordionTrigger,
} from "@/components/ui/accordion"; } from '@/components/ui/accordion'
import { Container } from "./Container"; import { Container } from './Container'
const faqs = [ const faqs = [
{ {
question: "What is Dokploy?", 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.", 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?", 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?", 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.", 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?", 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.", 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?", 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.", answer: 'No, there is no limit on the number of deployments in any of the plans.',
}, },
{ {
question: "What happens if I exceed my purchased server limit?", question: 'What happens if I exceed my purchased server limit?',
answer: "The most recently added servers will be deactivated. You won't be able to create services on inactive servers until they are reactivated.", 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?", 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).", 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?", 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?", question: 'Why Choose Dokploy?',
answer: "Dokploy offers simplicity, flexibility, and speed in application deployment and management.", answer: 'Dokploy offers simplicity, flexibility, and speed in application deployment and management.',
}, },
{ {
question: "Is it open source?", question: 'Is it open source?',
answer: "Yes, Dokploy is open source and free to use.", answer: 'Yes, Dokploy is open source and free to use.',
}, },
{ {
question: "What types of languages can I deploy with Dokploy?", 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.", 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?", 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.", 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.", answer: "No, we don't track any usage data.",
}, },
{ {
question: "Are there any user forums or communities where I can interact with other users?", question:
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?", 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.", 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?", 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.", 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?", 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.", 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() { export function Faqs() {
return ( return (
@@ -85,25 +86,30 @@ export function Faqs() {
className="relative overflow-hidden bg-black py-20 sm:py-32" className="relative overflow-hidden bg-black py-20 sm:py-32"
> >
<Container className="relative flex flex-col gap-10"> <Container className="relative flex flex-col gap-10">
<div className="mx-auto lg:mx-0 justify-center w-full"> <div className="mx-auto w-full justify-center lg:mx-0">
<h2 <h2
id="faq-title" id="faq-title"
className="font-display text-3xl tracking-tight text-primary sm:text-4xl text-center" className="text-center font-display text-3xl tracking-tight text-primary sm:text-4xl"
> >
Frequently asked questions Frequently asked questions
</h2> </h2>
<p className="mt-4 text-lg tracking-tight text-muted-foreground text-center"> <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> </p>
</div> </div>
<Accordion <Accordion
type="single" type="single"
collapsible collapsible
className="w-full max-w-3xl mx-auto" className="mx-auto w-full max-w-3xl"
> >
{faqs.map((faq, columnIndex) => ( {faqs.map((faq, columnIndex) => (
<AccordionItem value={`${columnIndex}`} key={columnIndex}> <AccordionItem
value={`${columnIndex}`}
key={columnIndex}
>
<AccordionTrigger className="text-left"> <AccordionTrigger className="text-left">
{faq.question} {faq.question}
</AccordionTrigger> </AccordionTrigger>
@@ -113,5 +119,5 @@ export function Faqs() {
</Accordion> </Accordion>
</Container> </Container>
</section> </section>
); )
} }

View File

@@ -1,11 +1,11 @@
"use client"; 'use client'
import Link from "next/link"; import Link from 'next/link'
import type { SVGProps } from "react"; import type { SVGProps } from 'react'
import { Container } from "./Container"; import { Container } from './Container'
import { NavLink } from "./NavLink"; import { NavLink } from './NavLink'
import { Logo } from "./shared/Logo"; import { Logo } from './shared/Logo'
import { buttonVariants } from "./ui/button"; import { buttonVariants } from './ui/button'
const I18nIcon = (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => ( const I18nIcon = (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
<svg <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" 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> </svg>
); )
export function Footer() { export function Footer() {
return ( return (
@@ -51,7 +51,7 @@ export function Footer() {
</nav> </nav>
</div> </div>
<div className="flex flex-col items-center border-t border-slate-400/10 py-10 sm:flex-row-reverse sm:justify-between"> <div className="flex flex-col items-center border-t border-slate-400/10 py-10 sm:flex-row-reverse sm:justify-between">
<div className="flex gap-x-6 items-center"> <div className="flex items-center gap-x-6">
<Link <Link
href="https://x.com/getdokploy" href="https://x.com/getdokploy"
className="group" className="group"
@@ -87,5 +87,5 @@ export function Footer() {
</div> </div>
</Container> </Container>
</footer> </footer>
); )
} }

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
"use client"; 'use client'
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils'
import { motion } from "framer-motion"; import { motion } from 'framer-motion'
import { Check, ChevronRight, Copy } from "lucide-react"; import { Check, ChevronRight, Copy } from 'lucide-react'
import Link from "next/link"; import Link from 'next/link'
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react'
import AnimatedGradientText from "./ui/animated-gradient-text"; import AnimatedGradientText from './ui/animated-gradient-text'
import AnimatedGridPattern from "./ui/animated-grid-pattern"; import AnimatedGridPattern from './ui/animated-grid-pattern'
import { Button } from "./ui/button"; import { Button } from './ui/button'
import HeroVideoDialog from "./ui/hero-video-dialog"; import HeroVideoDialog from './ui/hero-video-dialog'
// const ProductHunt = () => { // const ProductHunt = () => {
// return ( // return (
@@ -42,14 +42,14 @@ import HeroVideoDialog from "./ui/hero-video-dialog";
// }; // };
export function Hero() { export function Hero() {
const [isCopied, setIsCopied] = useState(false); const [isCopied, setIsCopied] = useState(false)
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
setIsCopied(false); setIsCopied(false)
}, 2000); }, 2000)
return () => clearTimeout(timer); return () => clearTimeout(timer)
}, [isCopied]); }, [isCopied])
return ( return (
<div className="h-[1100px] bg-black pt-20 sm:h-[1100px] lg:pt-32"> <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"> <div className=" bottom-0 flex w-full items-center justify-center overflow-hidden rounded-lg bg-background md:shadow-xl">
@@ -77,34 +77,38 @@ export function Hero() {
</div> </div>
</motion.a> */} </motion.a> */}
<motion.h1 <motion.h1
className="mx-auto max-w-4xl font-display text-5xl font-medium tracking-tight text-muted-foreground sm:text-7xl" className="mx-auto max-w-4xl font-display text-5xl font-medium tracking-tight text-muted-foreground sm:text-7xl"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
>
Simplify{" "}
<span className="relative whitespace-nowrap text-primary">
<svg
aria-hidden="true"
viewBox="0 0 418 42"
className="absolute left-0 top-2/3 h-[0.58em] w-full fill-primary"
preserveAspectRatio="none"
> >
<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" /> Simplify{' '}
</svg> <span className="relative whitespace-nowrap text-primary">
<span className="relative">Application and Database</span> <svg
</span>{" "} aria-hidden="true"
Deployments viewBox="0 0 418 42"
</motion.h1> className="absolute left-0 top-2/3 h-[0.58em] w-full fill-primary"
<motion.p preserveAspectRatio="none"
className="mx-auto mt-6 max-w-2xl text-lg tracking-tight text-muted-foreground" >
initial={{ opacity: 0, y: 20 }} <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" />
animate={{ opacity: 1, y: 0 }} </svg>
transition={{ duration: 0.3, delay: 0.2 }} <span className="relative">
> Application and Database
Manage containerized deployments across multiple servers with ease thanks to our all-in-one platform for developers. </span>
</motion.p> </span>{' '}
Deployments
</motion.h1>
<motion.p
className="mx-auto mt-6 max-w-2xl text-lg tracking-tight text-muted-foreground"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.2 }}
>
Manage containerized deployments across multiple
servers with ease thanks to our all-in-one platform
for developers.
</motion.p>
<motion.div <motion.div
className="flex flex-col items-center justify-center space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0" className="flex flex-col items-center justify-center space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
@@ -114,16 +118,21 @@ export function Hero() {
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="mt-6 flex flex-wrap items-center justify-center gap-6 md:flex-nowrap"> <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"> <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 <button
type="button" type="button"
onClick={() => onClick={() =>
navigator.clipboard navigator.clipboard
.writeText( .writeText(
"curl -sSL https://dokploy.com/install.sh | sh", 'curl -sSL https://dokploy.com/install.sh | sh',
)
.then(() =>
setIsCopied(true),
)
.catch(() =>
setIsCopied(false),
) )
.then(() => setIsCopied(true))
.catch(() => setIsCopied(false))
} }
> >
{isCopied ? ( {isCopied ? (
@@ -135,14 +144,20 @@ export function Hero() {
</code> </code>
</div> </div>
<div className="mx-auto flex w-full max-w-sm flex-wrap items-center justify-center gap-3 md:flex-nowrap"> <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 <Link
href="https://github.com/dokploy/dokploy" href="https://github.com/dokploy/dokploy"
aria-label="Dokploy on GitHub" aria-label="Dokploy on GitHub"
target="_blank" target="_blank"
className="flex flex-row items-center gap-2" 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" /> <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> </svg>
GitHub GitHub
@@ -198,11 +213,11 @@ export function Hero() {
duration={3} duration={3}
repeatDelay={1} repeatDelay={1}
className={cn( className={cn(
"[mask-image:radial-gradient(800px_circle_at_center,white,transparent)]", '[mask-image:radial-gradient(800px_circle_at_center,white,transparent)]',
"absolute inset-x-0 inset-y-[-30%] h-[200%] skew-y-12", 'absolute inset-x-0 inset-y-[-30%] h-[200%] skew-y-12',
)} )}
/> />
</div> </div>
</div> </div>
); )
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,17 @@
"use client"; 'use client'
import { useEffect } from "react"; import { useEffect } from 'react'
import initializeGA from "."; import initializeGA from '.'
export default function GoogleAnalytics() { export default function GoogleAnalytics() {
useEffect(() => { useEffect(() => {
// @ts-ignore // @ts-ignore
if (!window.GA_INITIALIZED) { if (!window.GA_INITIALIZED) {
initializeGA(); initializeGA()
// @ts-ignore // @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 = () => { const initializeGA = () => {
// Replace with your Measurement ID // Replace with your Measurement ID
// It ideally comes from an environment variable // It ideally comes from an environment variable
ReactGA.initialize("G-0RTZ5EPB26"); ReactGA.initialize('G-0RTZ5EPB26')
// Don't forget to remove the console.log() statements // Don't forget to remove the console.log() statements
// when you are done // when you are done
}; }
interface Props { interface Props {
category: string; category: string
action: string; action: string
label: string; label: string
} }
const trackGAEvent = ({ category, action, label }: Props) => { const trackGAEvent = ({ category, action, label }: Props) => {
console.log("GA event:", category, ":", action, ":", label); console.log('GA event:', category, ':', action, ':', label)
// Send GA4 Event // Send GA4 Event
ReactGA.event({ ReactGA.event({
category: category, category: category,
action: action, action: action,
label: label, label: label,
}); })
}; }
export default initializeGA; export default initializeGA
export { initializeGA, trackGAEvent }; export { initializeGA, trackGAEvent }

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
"use client"; 'use client'
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils'
import { import {
IconActivity, IconActivity,
IconCloud, IconCloud,
@@ -10,101 +10,101 @@ import {
IconTerminal, IconTerminal,
IconTerminal2, IconTerminal2,
IconUsers, IconUsers,
} from "@tabler/icons-react"; } from '@tabler/icons-react'
import { Layers, Lock, UnlockIcon } from "lucide-react"; import { Layers, Lock, UnlockIcon } from 'lucide-react'
export function FirstFeaturesSection() { export function FirstFeaturesSection() {
const features = [ const features = [
{ {
title: "Flexible Application Deployment", title: 'Flexible Application Deployment',
description: 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 />, icon: <IconRocket />,
}, },
{ {
title: "Native Docker Compose Support", title: 'Native Docker Compose Support',
description: 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 />, icon: <Layers />,
}, },
{ {
title: "Multi-server Support", title: 'Multi-server Support',
description: 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 />, icon: <IconCloud />,
}, },
{ {
title: "Advanced User Management", title: 'Advanced User Management',
description: 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 />, icon: <IconUsers />,
}, },
{ {
title: "Database Management with Backups", title: 'Database Management with Backups',
description: 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 />, icon: <IconDatabase />,
}, },
{ {
title: "API & CLI Access", title: 'API & CLI Access',
description: 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 />, icon: <IconTerminal />,
}, },
{ {
title: "Docker Swarm Clusters", title: 'Docker Swarm Clusters',
description: 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 />, icon: <IconUsers />,
}, },
{ {
title: "Open Source Templates", title: 'Open Source Templates',
description: 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 />, icon: <IconTemplate />,
}, },
{ {
title: "No Vendor Lock-In", title: 'No Vendor Lock-In',
description: 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 />, icon: <UnlockIcon />,
}, },
{ {
title: "Real-time Monitoring & Alerts", title: 'Real-time Monitoring & Alerts',
description: 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 />, icon: <IconActivity />,
}, },
{ {
title: "Built for Developers", title: 'Built for Developers',
description: description:
"Designed specifically for engineers and developers seeking control and flexibility.", 'Designed specifically for engineers and developers seeking control and flexibility.',
icon: <IconTerminal2 />, icon: <IconTerminal2 />,
}, },
{ {
title: "Self-hosted & Open Source", title: 'Self-hosted & Open Source',
description: 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 />, icon: <IconEaseInOut />,
}, },
]; ]
return ( return (
<div className="flex flex-col justify-center items-center mt-20 px-4"> <div className="mt-20 flex flex-col items-center justify-center px-4">
<h2 className="font-display text-3xl tracking-tight text-primary sm:text-4xl text-center"> <h2 className="text-center font-display text-3xl tracking-tight text-primary sm:text-4xl">
Powerful Deployment Tailored to You Powerful Deployment Tailored to You
</h2> </h2>
<p className="mt-4 text-lg tracking-tight text-muted-foreground text-center"> <p className="mt-4 text-center text-lg tracking-tight text-muted-foreground">
Unlock seamless multi-server deployments, advanced user control, and Unlock seamless multi-server deployments, advanced user control,
flexible database managementall with Dokploys developer-focused and flexible database managementall with Dokploys
features. developer-focused features.
</p> </p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 relative z-10 py-10 max-w-7xl mx-auto mt-10 max-sm:p-0 max-sm:mx-0 max-sm:w-full"> <div className="relative z-10 mx-auto mt-10 grid max-w-7xl grid-cols-1 py-10 max-sm:mx-0 max-sm:w-full max-sm:p-0 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{features.map((feature, index) => ( {features.map((feature, index) => (
<Feature key={feature.title} {...feature} index={index} /> <Feature key={feature.title} {...feature} index={index} />
))} ))}
</div> </div>
</div> </div>
); )
} }
const Feature = ({ const Feature = ({
@@ -113,36 +113,39 @@ const Feature = ({
icon, icon,
index, index,
}: { }: {
title: string; title: string
description: string; description: string
icon: React.ReactNode; icon: React.ReactNode
index: number; index: number
}) => { }) => {
return ( return (
<div <div
className={cn( className={cn(
"flex flex-col lg:border-r py-10 relative group/feature border-neutral-800", 'group/feature relative flex flex-col border-neutral-800 py-10 lg:border-r',
(index === 0 || index === 4 || index === 8) && (index === 0 || index === 4 || index === 8) &&
"lg:border-l dark:border-neutral-800", 'dark:border-neutral-800 lg:border-l',
(index < 4 || index < 8) && "lg:border-b dark:border-neutral-800", (index < 4 || index < 8) &&
'dark:border-neutral-800 lg:border-b',
)} )}
> >
{index < 4 && ( {index < 4 && (
<div className="opacity-0 group-hover/feature:opacity-100 transition duration-200 absolute inset-0 h-full w-full bg-gradient-to-t from-neutral-800 to-transparent pointer-events-none" /> <div className="pointer-events-none absolute inset-0 h-full w-full bg-gradient-to-t from-neutral-800 to-transparent opacity-0 transition duration-200 group-hover/feature:opacity-100" />
)} )}
{index >= 4 && ( {index >= 4 && (
<div className="opacity-0 group-hover/feature:opacity-100 transition duration-200 absolute inset-0 h-full w-full bg-gradient-to-b from-neutral-800 to-transparent pointer-events-none" /> <div className="pointer-events-none absolute inset-0 h-full w-full bg-gradient-to-b from-neutral-800 to-transparent opacity-0 transition duration-200 group-hover/feature:opacity-100" />
)} )}
<div className="mb-4 relative z-10 px-10 text-neutral-400">{icon}</div> <div className="relative z-10 mb-4 px-10 text-neutral-400">
<div className="text-lg font-bold mb-2 relative z-10 px-10"> {icon}
<div className="absolute left-0 inset-y-0 h-6 group-hover/feature:h-8 w-1 rounded-tr-full rounded-br-full bg-neutral-700 group-hover/feature:bg-white transition-all duration-200 origin-center" /> </div>
<span className="group-hover/feature:translate-x-2 transition duration-200 inline-block text-neutral-100"> <div className="relative z-10 mb-2 px-10 text-lg font-bold">
<div className="absolute inset-y-0 left-0 h-6 w-1 origin-center rounded-br-full rounded-tr-full bg-neutral-700 transition-all duration-200 group-hover/feature:h-8 group-hover/feature:bg-white" />
<span className="inline-block text-neutral-100 transition duration-200 group-hover/feature:translate-x-2">
{title} {title}
</span> </span>
</div> </div>
<p className="text-sm text-neutral-300 lg:max-w-xs relative z-10 px-10"> <p className="relative z-10 px-10 text-sm text-neutral-300 lg:max-w-xs">
{description} {description}
</p> </p>
</div> </div>
); )
}; }

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -1,80 +1,87 @@
"use client"; 'use client'
import { Tab } from "@headlessui/react"; import { Tab } from '@headlessui/react'
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from 'framer-motion'
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react'
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils'
const features = [ 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.", description:
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.", description:
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.", description:
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.", description:
image: "/logs.png", "Monitor and manage your applications' logs with ease, ensuring efficient troubleshooting and optimal performance.",
image: '/logs.png',
}, },
{ {
title: "Monitoring", title: 'Monitoring',
description: "Monitor your systems' performance and health in real time, ensuring continuous and uninterrupted operation.", description:
image: "/primary/monitoring.png", "Monitor your systems' performance and health in real time, ensuring continuous and uninterrupted operation.",
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.", description:
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.", description:
image: "/traefik.png", 'Manage Traefik via File Editor to configure your own domain names, certificates, and more.',
image: '/traefik.png',
}, },
{ {
title: "Templates", title: 'Templates',
description: "Deploy open source templates with one click.", description: 'Deploy open source templates with one click.',
image: "/templates.png", image: '/templates.png',
}, },
]; ]
export function SecondaryFeaturesSections() { export function SecondaryFeaturesSections() {
const [tabOrientation, setTabOrientation] = useState< const [tabOrientation, setTabOrientation] = useState<
"horizontal" | "vertical" 'horizontal' | 'vertical'
>("horizontal"); >('horizontal')
useEffect(() => { useEffect(() => {
const lgMediaQuery = window.matchMedia("(min-width: 1024px)"); const lgMediaQuery = window.matchMedia('(min-width: 1024px)')
function onMediaQueryChange({ matches }: { matches: boolean }) { function onMediaQueryChange({ matches }: { matches: boolean }) {
setTabOrientation(matches ? "vertical" : "horizontal"); setTabOrientation(matches ? 'vertical' : 'horizontal')
} }
onMediaQueryChange(lgMediaQuery); onMediaQueryChange(lgMediaQuery)
lgMediaQuery.addEventListener("change", onMediaQueryChange); lgMediaQuery.addEventListener('change', onMediaQueryChange)
return () => { 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 // Cambiar isMounted a true después del primer render
useEffect(() => { useEffect(() => {
setIsMounted(true); setIsMounted(true)
}, []); }, [])
return ( return (
<section <section
@@ -82,13 +89,15 @@ export function SecondaryFeaturesSections() {
aria-label="Features for running your books" aria-label="Features for running your books"
className="relative overflow-hidden bg-black pb-28 pt-20 sm:py-32" className="relative overflow-hidden bg-black pb-28 pt-20 sm:py-32"
> >
<div className="mx-auto max-w-7xl max-lg:px-4 relative"> <div className="relative mx-auto max-w-7xl max-lg:px-4">
<div className="max-w-2xl md:mx-auto md:text-center xl:max-w-none"> <div className="max-w-2xl md:mx-auto md:text-center xl:max-w-none">
<h2 className="font-display text-3xl tracking-tight text-white sm:text-4xl md:text-5xl"> <h2 className="font-display text-3xl tracking-tight text-white sm:text-4xl md:text-5xl">
Comprehensive Control of Your Digital Ecosystem Comprehensive Control of Your Digital Ecosystem
</h2> </h2>
<p className="mt-6 text-lg tracking-tight text-muted-foreground"> <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> </p>
</div> </div>
<Tab.Group <Tab.Group
@@ -98,7 +107,7 @@ export function SecondaryFeaturesSections() {
> >
{({ selectedIndex }) => ( {({ selectedIndex }) => (
<> <>
<div className="-mx-4 flex overflow-x-auto pb-4 sm:mx-0 overflow-visible sm:pb-0"> <div className="-mx-4 flex overflow-visible overflow-x-auto pb-4 sm:mx-0 sm:pb-0">
<Tab.List <Tab.List
aria-description="primary feature tabs" aria-description="primary feature tabs"
aria-roledescription="primary feature tabs" aria-roledescription="primary feature tabs"
@@ -110,11 +119,12 @@ export function SecondaryFeaturesSections() {
initial={false} initial={false}
key={`feature-${featureIndex}`} key={`feature-${featureIndex}`}
className={cn( className={cn(
"group relative rounded-full px-4 py-1 transition-colors ", 'group relative rounded-full px-4 py-1 transition-colors ',
)} )}
> >
<AnimatePresence> <AnimatePresence>
{selectedIndex === featureIndex && ( {selectedIndex ===
featureIndex && (
<motion.span <motion.span
layoutId="tab" layoutId="tab"
className="absolute inset-0 z-10 rounded-full bg-white/5 mix-blend-difference" className="absolute inset-0 z-10 rounded-full bg-white/5 mix-blend-difference"
@@ -122,7 +132,7 @@ export function SecondaryFeaturesSections() {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ transition={{
type: "spring", type: 'spring',
bounce: 0.2, bounce: 0.2,
duration: 0.5, duration: 0.5,
}} }}
@@ -132,7 +142,7 @@ export function SecondaryFeaturesSections() {
<h3> <h3>
<Tab <Tab
className={cn( 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" /> <span className="absolute inset-0 rounded-full" />
@@ -141,7 +151,7 @@ export function SecondaryFeaturesSections() {
</h3> </h3>
<p <p
className={cn( className={cn(
"mt-2 hidden text-sm text-muted-foreground ", 'mt-2 hidden text-sm text-muted-foreground ',
)} )}
> >
{feature.description} {feature.description}
@@ -155,18 +165,24 @@ export function SecondaryFeaturesSections() {
<Tab.Panel key={`panel-${index}`}> <Tab.Panel key={`panel-${index}`}>
<div className="relative sm:px-6 "> <div className="relative sm:px-6 ">
<div className="absolute -inset-x-4 bottom-[-4.25rem] top-[-6.5rem] bg-card/60 ring-1 ring-inset ring-white/10 sm:inset-x-0 sm:rounded-t-xl" /> <div className="absolute -inset-x-4 bottom-[-4.25rem] top-[-6.5rem] bg-card/60 ring-1 ring-inset ring-white/10 sm:inset-x-0 sm:rounded-t-xl" />
<p className="relative mx-auto max-w-2xl text-base text-white sm:text-center mb-10"> <p className="relative mx-auto mb-10 max-w-2xl text-base text-white sm:text-center">
{feature.description} {feature.description}
</p> </p>
</div> </div>
<motion.div <motion.div
key={feature.title} key={feature.title}
initial={isMounted ? { opacity: 0.4 } : {}} initial={
animate={isMounted ? { opacity: 1 } : {}} isMounted
? { opacity: 0.4 }
: {}
}
animate={
isMounted ? { opacity: 1 } : {}
}
exit={{ opacity: 0, x: -50 }} exit={{ opacity: 0, x: -50 }}
transition={{ transition={{
type: "spring", type: 'spring',
bounce: 0.2, bounce: 0.2,
duration: 0.8, duration: 0.8,
}} }}
@@ -174,13 +190,16 @@ export function SecondaryFeaturesSections() {
> >
<div className="relative w-full"> <div className="relative w-full">
<div className="mx-auto"> <div className="mx-auto">
<div className="w-full h-11 rounded-t-lg bg-card flex justify-start items-center space-x-1.5 px-3"> <div className="flex h-11 w-full items-center justify-start space-x-1.5 rounded-t-lg bg-card px-3">
<span className="w-3 h-3 rounded-full bg-red-400" /> <span className="h-3 w-3 rounded-full bg-red-400" />
<span className="w-3 h-3 rounded-full bg-yellow-400" /> <span className="h-3 w-3 rounded-full bg-yellow-400" />
<span className="w-3 h-3 rounded-full bg-green-400" /> <span className="h-3 w-3 rounded-full bg-green-400" />
</div> </div>
<div className="bg-gray-100 w-full h-96"> <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> </div>
</div> </div>
@@ -193,5 +212,5 @@ export function SecondaryFeaturesSections() {
</Tab.Group> </Tab.Group>
</div> </div>
</section> </section>
); )
} }

View File

@@ -1,4 +1,4 @@
export function Logo(props: React.ComponentPropsWithoutRef<"svg">) { export function Logo(props: React.ComponentPropsWithoutRef<'svg'>) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/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" 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> </svg>
); )
} }

View File

@@ -1,24 +1,27 @@
"use client"; 'use client'
import { PlusCircleIcon } from "lucide-react"; import { PlusCircleIcon } from 'lucide-react'
import Link from "next/link"; import Link from 'next/link'
import { buttonVariants } from "./ui/button"; import { buttonVariants } from './ui/button'
import Ripple from "./ui/ripple"; import Ripple from './ui/ripple'
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "./ui/tooltip"; } from './ui/tooltip'
export const Sponsors = () => { export const Sponsors = () => {
return ( return (
<div className="mt-20 flex flex-col justify-center gap-y-10 w-full "> <div className="mt-20 flex w-full flex-col justify-center gap-y-10 ">
<div className="flex flex-col justify-start gap-4 px-4"> <div className="flex flex-col justify-start gap-4 px-4">
<h3 className="mx-auto max-w-2xl font-display text-3xl font-medium tracking-tight text-primary sm:text-5xl text-center"> <h3 className="mx-auto max-w-2xl text-center font-display text-3xl font-medium tracking-tight text-primary sm:text-5xl">
Sponsors Sponsors
</h3> </h3>
<p className="mx-auto max-w-2xl text-lg tracking-tight text-muted-foreground text-center"> <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> </p>
</div> </div>
<div className="relative flex h-[700px] w-full flex-col items-center justify-center overflow-hidden bg-background md:shadow-xl"> <div className="relative flex h-[700px] w-full flex-col items-center justify-center overflow-hidden bg-background md:shadow-xl">
@@ -26,18 +29,19 @@ export const Sponsors = () => {
<Tooltip> <Tooltip>
<TooltipTrigger className="z-10 m-0 p-0"> <TooltipTrigger className="z-10 m-0 p-0">
<Link <Link
href={"https://opencollective.com/dokploy"} href={'https://opencollective.com/dokploy'}
target="_blank" target="_blank"
className={buttonVariants({ className={buttonVariants({
variant: "secondary", variant: 'secondary',
size: "sm", size: 'sm',
className: "bg-transparent !rounded-full w-fit !p-0 m-0", className:
'm-0 w-fit !rounded-full bg-transparent !p-0',
})} })}
> >
<PlusCircleIcon className="size-10 text-muted-foreground hover:text-primary transition-colors" /> <PlusCircleIcon className="size-10 text-muted-foreground transition-colors hover:text-primary" />
</Link> </Link>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="bg-black rounded-lg border-0 text-center w-[200px] z-[200] text-white font-semibold"> <TooltipContent className="z-[200] w-[200px] rounded-lg border-0 bg-black text-center font-semibold text-white">
Become a sponsor 🤑 Become a sponsor 🤑
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@@ -45,5 +49,5 @@ export const Sponsors = () => {
<Ripple /> <Ripple />
</div> </div>
</div> </div>
); )
}; }

View File

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

View File

@@ -1,12 +1,12 @@
"use client"; 'use client'
import * as AccordionPrimitive from "@radix-ui/react-accordion"; import * as AccordionPrimitive from '@radix-ui/react-accordion'
import { ChevronDown, Minus, PlusIcon } from "lucide-react"; import { ChevronDown, Minus, PlusIcon } from 'lucide-react'
import * as React from "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< const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>, React.ElementRef<typeof AccordionPrimitive.Item>,
@@ -14,11 +14,11 @@ const AccordionItem = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AccordionPrimitive.Item <AccordionPrimitive.Item
ref={ref} ref={ref}
className={cn("border-b", className)} className={cn('border-b', className)}
{...props} {...props}
/> />
)); ))
AccordionItem.displayName = "AccordionItem"; AccordionItem.displayName = 'AccordionItem'
const AccordionTrigger = React.forwardRef< const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>, React.ElementRef<typeof AccordionPrimitive.Trigger>,
@@ -28,7 +28,7 @@ const AccordionTrigger = React.forwardRef<
<AccordionPrimitive.Trigger <AccordionPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180 group", 'group flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
className, className,
)} )}
{...props} {...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" /> <Minus className="h-4 w-4 shrink-0 transition-transform duration-200 group-data-[state=closed]:hidden" />
</AccordionPrimitive.Trigger> </AccordionPrimitive.Trigger>
</AccordionPrimitive.Header> </AccordionPrimitive.Header>
)); ))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef< const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>, 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" className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props} {...props}
> >
<div className={cn("pb-4 pt-0 text-muted-foreground", className)}> <div className={cn('pb-4 pt-0 text-muted-foreground', className)}>
{children} {children}
</div> </div>
</AccordionPrimitive.Content> </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({ export default function AnimatedGradientText({
children, children,
className, className,
}: { }: {
children: ReactNode; children: ReactNode
className?: string; className?: string
}) { }) {
return ( return (
<div <div
className={cn( className={cn(
"group relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-2xl px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#8fdfff1f] backdrop-blur-sm transition-shadow duration-500 ease-out [--bg-size:300%] hover:shadow-[inset_0_-5px_10px_#8fdfff3f] bg-black/40", 'group relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-2xl bg-black/40 px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#8fdfff1f] backdrop-blur-sm transition-shadow duration-500 ease-out [--bg-size:300%] hover:shadow-[inset_0_-5px_10px_#8fdfff3f]',
className, className,
)} )}
> >
<div <div
className={ className={
"absolute inset-0 block h-full w-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] p-[1px] ![mask-composite:subtract] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]" 'absolute inset-0 block h-full w-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] p-[1px] [border-radius:inherit] ![mask-composite:subtract] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]'
} }
/> />
{children} {children}
</div> </div>
); )
} }

View File

@@ -1,21 +1,21 @@
"use client"; 'use client'
import { motion } from "framer-motion"; import { motion } from 'framer-motion'
import { useEffect, useId, useRef, useState } from "react"; import { useEffect, useId, useRef, useState } from 'react'
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils'
interface GridPatternProps { interface GridPatternProps {
width?: number; width?: number
height?: number; height?: number
x?: number; x?: number
y?: number; y?: number
strokeDasharray?: any; strokeDasharray?: any
numSquares?: number; numSquares?: number
className?: string; className?: string
maxOpacity?: number; maxOpacity?: number
duration?: number; duration?: number
repeatDelay?: number; repeatDelay?: number
} }
export function GridPattern({ export function GridPattern({
@@ -31,16 +31,16 @@ export function GridPattern({
repeatDelay = 0.5, repeatDelay = 0.5,
...props ...props
}: GridPatternProps) { }: GridPatternProps) {
const id = useId(); const id = useId()
const containerRef = useRef(null); const containerRef = useRef(null)
const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
const [squares, setSquares] = useState(() => generateSquares(numSquares)); const [squares, setSquares] = useState(() => generateSquares(numSquares))
function getPos() { function getPos() {
return [ return [
Math.floor((Math.random() * dimensions.width) / width), Math.floor((Math.random() * dimensions.width) / width),
Math.floor((Math.random() * dimensions.height) / height), Math.floor((Math.random() * dimensions.height) / height),
]; ]
} }
// Adjust the generateSquares function to return objects with an id, x, and y // 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) => ({ return Array.from({ length: count }, (_, i) => ({
id: i, id: i,
pos: getPos(), pos: getPos(),
})); }))
} }
// Function to update a single square's position // Function to update a single square's position
@@ -62,15 +62,15 @@ export function GridPattern({
} }
: sq, : sq,
), ),
); )
}; }
// Update squares to animate in // Update squares to animate in
useEffect(() => { useEffect(() => {
if (dimensions.width && dimensions.height) { if (dimensions.width && dimensions.height) {
setSquares(generateSquares(numSquares)); setSquares(generateSquares(numSquares))
} }
}, [dimensions, numSquares]); }, [dimensions, numSquares])
// Resize observer to update container dimensions // Resize observer to update container dimensions
useEffect(() => { useEffect(() => {
@@ -79,27 +79,27 @@ export function GridPattern({
setDimensions({ setDimensions({
width: entry.contentRect.width, width: entry.contentRect.width,
height: entry.contentRect.height, height: entry.contentRect.height,
}); })
} }
}); })
if (containerRef.current) { if (containerRef.current) {
resizeObserver.observe(containerRef.current); resizeObserver.observe(containerRef.current)
} }
return () => { return () => {
if (containerRef.current) { if (containerRef.current) {
resizeObserver.unobserve(containerRef.current); resizeObserver.unobserve(containerRef.current)
} }
}; }
}, [containerRef]); }, [containerRef])
return ( return (
<svg <svg
ref={containerRef} ref={containerRef}
aria-hidden="true" aria-hidden="true"
className={cn( 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, className,
)} )}
{...props} {...props}
@@ -130,7 +130,7 @@ export function GridPattern({
duration, duration,
repeat: 1, repeat: 1,
delay: index * 0.1, delay: index * 0.1,
repeatType: "reverse", repeatType: 'reverse',
}} }}
onAnimationComplete={() => updateSquarePosition(id)} onAnimationComplete={() => updateSquarePosition(id)}
key={`${x}-${y}-${index}`} key={`${x}-${y}-${index}`}
@@ -144,7 +144,7 @@ export function GridPattern({
))} ))}
</svg> </svg>
</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 { interface AnimatedShinyTextProps {
children: ReactNode; children: ReactNode
className?: string; className?: string
shimmerWidth?: number; shimmerWidth?: number
} }
const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({ const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
@@ -17,24 +17,24 @@ const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
<p <p
style={ style={
{ {
"--shiny-width": `${shimmerWidth}px`, '--shiny-width': `${shimmerWidth}px`,
} as CSSProperties } as CSSProperties
} }
className={cn( 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 // 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 // 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, className,
)} )}
> >
{children} {children}
</p> </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 AvatarPrimitive from '@radix-ui/react-avatar'
import * as React from "react"; import * as React from 'react'
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils'
const Avatar = React.forwardRef< const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>, React.ElementRef<typeof AvatarPrimitive.Root>,
@@ -12,13 +12,13 @@ const Avatar = React.forwardRef<
<AvatarPrimitive.Root <AvatarPrimitive.Root
ref={ref} ref={ref}
className={cn( 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, className,
)} )}
{...props} {...props}
/> />
)); ))
Avatar.displayName = AvatarPrimitive.Root.displayName; Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef< const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>, React.ElementRef<typeof AvatarPrimitive.Image>,
@@ -26,11 +26,11 @@ const AvatarImage = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Image <AvatarPrimitive.Image
ref={ref} ref={ref}
className={cn("aspect-square h-full w-full", className)} className={cn('aspect-square h-full w-full', className)}
{...props} {...props}
/> />
)); ))
AvatarImage.displayName = AvatarPrimitive.Image.displayName; AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef< const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>, React.ElementRef<typeof AvatarPrimitive.Fallback>,
@@ -39,12 +39,12 @@ const AvatarFallback = React.forwardRef<
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
ref={ref} ref={ref}
className={cn( 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, className,
)} )}
{...props} {...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 VariantProps, cva } from 'class-variance-authority'
import type * as React from "react"; import type * as React from 'react'
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils'
const badgeVariants = cva( 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: { variants: {
variant: { variant: {
default: default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary: secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive: destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: "text-foreground", outline: 'text-foreground',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
}, },
}, },
); )
export interface BadgeProps export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>, extends React.HTMLAttributes<HTMLDivElement>,
@@ -30,7 +30,7 @@ export interface BadgeProps
function Badge({ className, variant, ...props }: BadgeProps) { function Badge({ className, variant, ...props }: BadgeProps) {
return ( return (
<div className={cn(badgeVariants({ variant }), className)} {...props} /> <div className={cn(badgeVariants({ variant }), className)} {...props} />
); )
} }
export { Badge, badgeVariants }; export { Badge, badgeVariants }

View File

@@ -1,59 +1,60 @@
"use client"; 'use client'
import { Slot } from "@radix-ui/react-slot"; import { Slot } from '@radix-ui/react-slot'
import { type VariantProps, cva } from "class-variance-authority"; import { type VariantProps, cva } from 'class-variance-authority'
import * as React from "react"; import * as React from 'react'
import { cn } from "../../lib/utils"; import { cn } from '../../lib/utils'
const buttonVariants = cva( 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: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default:
'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90", 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 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: secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80", 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: 'hover:bg-accent hover:text-accent-foreground',
link: "text-primary underline-offset-4 hover:underline", link: 'text-primary underline-offset-4 hover:underline',
}, },
size: { size: {
default: "h-10 px-4 py-2", default: 'h-10 px-4 py-2',
sm: "h-9 rounded-md px-3", sm: 'h-9 rounded-md px-3',
lg: "h-11 rounded-md px-8", lg: 'h-11 rounded-md px-8',
icon: "h-10 w-10", icon: 'h-10 w-10',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
size: "default", size: 'default',
}, },
}, },
); )
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
asChild?: boolean; asChild?: boolean
children?: React.ReactNode; children?: React.ReactNode
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"; const Comp = asChild ? Slot : 'button'
return ( return (
<Comp <Comp
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
ref={ref} ref={ref}
{...props} {...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 { CheckIcon, CopyIcon } from 'lucide-react'
import { useState } from "react"; import { useState } from 'react'
interface CopyButtonProps { interface CopyButtonProps {
text: string; text: string
} }
export function CopyButton({ text }: CopyButtonProps) { export function CopyButton({ text }: CopyButtonProps) {
const [isCopied, setIsCopied] = useState(false); const [isCopied, setIsCopied] = useState(false)
const copy = async () => { const copy = async () => {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text)
setIsCopied(true); setIsCopied(true)
setTimeout(() => setIsCopied(false), 2000); setTimeout(() => setIsCopied(false), 2000)
}; }
return ( return (
<button <button
@@ -28,5 +28,5 @@ export function CopyButton({ text }: CopyButtonProps) {
<CopyIcon className="h-full w-full text-gray-400" /> <CopyIcon className="h-full w-full text-gray-400" />
)} )}
</button> </button>
); )
} }

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils'
import * as React from "react"; import * as React from 'react'
export interface InputProps export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> { extends React.InputHTMLAttributes<HTMLInputElement> {
errorMessage?: string; errorMessage?: string
} }
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.forwardRef<HTMLInputElement, InputProps>(
@@ -14,7 +14,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
type={type} type={type}
className={cn( className={cn(
// bg-gray // 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, className,
)} )}
ref={ref} ref={ref}
@@ -26,26 +26,28 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
</span> </span>
)} )}
</> </>
); )
}, },
); )
Input.displayName = "Input"; Input.displayName = 'Input'
const NumberInput = React.forwardRef<HTMLInputElement, InputProps>( const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
({ className, errorMessage, ...props }, ref) => { ({ className, errorMessage, ...props }, ref) => {
return ( return (
<Input <Input
type="text" type="text"
className={cn("text-left", className)} className={cn('text-left', className)}
ref={ref} ref={ref}
{...props} {...props}
value={props.value === undefined ? undefined : String(props.value)} value={
props.value === undefined ? undefined : String(props.value)
}
onChange={(e) => { onChange={(e) => {
const value = e.target.value; const value = e.target.value
if (value === "") { if (value === '') {
props.onChange?.(e); props.onChange?.(e)
} else { } else {
const number = Number.parseInt(value, 10); const number = Number.parseInt(value, 10)
if (!Number.isNaN(number)) { if (!Number.isNaN(number)) {
const syntheticEvent = { const syntheticEvent = {
...e, ...e,
@@ -53,17 +55,17 @@ const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
...e.target, ...e.target,
value: number, value: number,
}, },
}; }
props.onChange?.( props.onChange?.(
syntheticEvent as unknown as React.ChangeEvent<HTMLInputElement>, 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 { interface MarqueeProps {
className?: string; className?: string
reverse?: boolean; reverse?: boolean
pauseOnHover?: boolean; pauseOnHover?: boolean
children?: React.ReactNode; children?: React.ReactNode
vertical?: boolean; vertical?: boolean
repeat?: number; repeat?: number
[key: string]: any; [key: string]: any
} }
export function Marquee({ export function Marquee({
@@ -23,10 +23,10 @@ export function Marquee({
<div <div
{...props} {...props}
className={cn( 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-row': !vertical,
"flex-col": vertical, 'flex-col': vertical,
}, },
className, className,
)} )}
@@ -36,16 +36,20 @@ export function Marquee({
.map((_, i) => ( .map((_, i) => (
<div <div
key={i} key={i}
className={cn("flex shrink-0 justify-around [gap:var(--gap)]", { className={cn(
"animate-marquee flex-row": !vertical, 'flex shrink-0 justify-around [gap:var(--gap)]',
"animate-marquee-vertical flex-col": vertical, {
"group-hover:[animation-play-state:paused]": pauseOnHover, 'animate-marquee flex-row': !vertical,
"[animation-direction:reverse]": reverse, 'animate-marquee-vertical flex-col': vertical,
})} 'group-hover:[animation-play-state:paused]':
pauseOnHover,
'[animation-direction:reverse]': reverse,
},
)}
> >
{children} {children}
</div> </div>
))} ))}
</div> </div>
); )
} }

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
"use client"; 'use client'
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
import * as React from "react"; import * as React from 'react'
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils'
const ScrollArea = React.forwardRef< const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>, React.ElementRef<typeof ScrollAreaPrimitive.Root>,
@@ -11,7 +11,7 @@ const ScrollArea = React.forwardRef<
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root <ScrollAreaPrimitive.Root
ref={ref} ref={ref}
className={cn("relative overflow-hidden", className)} className={cn('relative overflow-hidden', className)}
{...props} {...props}
> >
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]"> <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
@@ -20,29 +20,31 @@ const ScrollArea = React.forwardRef<
<ScrollBar /> <ScrollBar />
<ScrollAreaPrimitive.Corner /> <ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root> </ScrollAreaPrimitive.Root>
)); ))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef< const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>, React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> React.ComponentPropsWithoutRef<
>(({ className, orientation = "vertical", ...props }, ref) => ( typeof ScrollAreaPrimitive.ScrollAreaScrollbar
>
>(({ className, orientation = 'vertical', ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar <ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref} ref={ref}
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"flex touch-none select-none transition-colors", 'flex touch-none select-none transition-colors',
orientation === "vertical" && orientation === 'vertical' &&
"h-full w-2.5 border-l border-l-transparent p-[1px]", 'h-full w-2.5 border-l border-l-transparent p-[1px]',
orientation === "horizontal" && orientation === 'horizontal' &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]", 'h-2.5 flex-col border-t border-t-transparent p-[1px]',
className, className,
)} )}
{...props} {...props}
> >
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" /> <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar> </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 * as SelectPrimitive from '@radix-ui/react-select'
import { Check, ChevronDown, ChevronUp } from "lucide-react"; import { Check, ChevronDown, ChevronUp } from 'lucide-react'
import * as React from "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< const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>, React.ElementRef<typeof SelectPrimitive.Trigger>,
@@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
ref={ref} ref={ref}
className={cn( 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, className,
)} )}
{...props} {...props}
@@ -29,8 +29,8 @@ const SelectTrigger = React.forwardRef<
<ChevronDown className="h-4 w-4 opacity-50" /> <ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon> </SelectPrimitive.Icon>
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
)); ))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef< const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
@@ -39,15 +39,15 @@ const SelectScrollUpButton = React.forwardRef<
<SelectPrimitive.ScrollUpButton <SelectPrimitive.ScrollUpButton
ref={ref} ref={ref}
className={cn( className={cn(
"flex cursor-default items-center justify-center py-1", 'flex cursor-default items-center justify-center py-1',
className, className,
)} )}
{...props} {...props}
> >
<ChevronUp className="h-4 w-4" /> <ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton> </SelectPrimitive.ScrollUpButton>
)); ))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef< const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
@@ -56,28 +56,28 @@ const SelectScrollDownButton = React.forwardRef<
<SelectPrimitive.ScrollDownButton <SelectPrimitive.ScrollDownButton
ref={ref} ref={ref}
className={cn( className={cn(
"flex cursor-default items-center justify-center py-1", 'flex cursor-default items-center justify-center py-1',
className, className,
)} )}
{...props} {...props}
> >
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton> </SelectPrimitive.ScrollDownButton>
)); ))
SelectScrollDownButton.displayName = SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName; SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef< const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>, React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => ( >(({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal> <SelectPrimitive.Portal>
<SelectPrimitive.Content <SelectPrimitive.Content
ref={ref} ref={ref}
className={cn( 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", '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" && 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", '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, className,
)} )}
position={position} position={position}
@@ -86,9 +86,9 @@ const SelectContent = React.forwardRef<
<SelectScrollUpButton /> <SelectScrollUpButton />
<SelectPrimitive.Viewport <SelectPrimitive.Viewport
className={cn( className={cn(
"p-1", 'p-1',
position === "popper" && position === 'popper' &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]", 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
)} )}
> >
{children} {children}
@@ -96,8 +96,8 @@ const SelectContent = React.forwardRef<
<SelectScrollDownButton /> <SelectScrollDownButton />
</SelectPrimitive.Content> </SelectPrimitive.Content>
</SelectPrimitive.Portal> </SelectPrimitive.Portal>
)); ))
SelectContent.displayName = SelectPrimitive.Content.displayName; SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef< const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>, React.ElementRef<typeof SelectPrimitive.Label>,
@@ -105,11 +105,11 @@ const SelectLabel = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.Label <SelectPrimitive.Label
ref={ref} 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} {...props}
/> />
)); ))
SelectLabel.displayName = SelectPrimitive.Label.displayName; SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef< const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>, React.ElementRef<typeof SelectPrimitive.Item>,
@@ -118,7 +118,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item <SelectPrimitive.Item
ref={ref} ref={ref}
className={cn( 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, className,
)} )}
{...props} {...props}
@@ -131,8 +131,8 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item> </SelectPrimitive.Item>
)); ))
SelectItem.displayName = SelectPrimitive.Item.displayName; SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef< const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>, React.ElementRef<typeof SelectPrimitive.Separator>,
@@ -140,11 +140,11 @@ const SelectSeparator = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.Separator <SelectPrimitive.Separator
ref={ref} 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} {...props}
/> />
)); ))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName; SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export { export {
Select, Select,
@@ -157,4 +157,4 @@ export {
SelectSeparator, SelectSeparator,
SelectScrollUpButton, SelectScrollUpButton,
SelectScrollDownButton, SelectScrollDownButton,
}; }

View File

@@ -1,9 +1,9 @@
"use client"; 'use client'
import * as SwitchPrimitives from "@radix-ui/react-switch"; import * as SwitchPrimitives from '@radix-ui/react-switch'
import * as React from "react"; import * as React from 'react'
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils'
const Switch = React.forwardRef< const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>, React.ElementRef<typeof SwitchPrimitives.Root>,
@@ -11,7 +11,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SwitchPrimitives.Root <SwitchPrimitives.Root
className={cn( 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, className,
)} )}
{...props} {...props}
@@ -19,11 +19,11 @@ const Switch = React.forwardRef<
> >
<SwitchPrimitives.Thumb <SwitchPrimitives.Thumb
className={cn( 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> </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 TabsPrimitive from '@radix-ui/react-tabs'
import * as React from "react"; 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< const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>, React.ElementRef<typeof TabsPrimitive.List>,
@@ -12,13 +12,13 @@ const TabsList = React.forwardRef<
<TabsPrimitive.List <TabsPrimitive.List
ref={ref} ref={ref}
className={cn( 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, className,
)} )}
{...props} {...props}
/> />
)); ))
TabsList.displayName = TabsPrimitive.List.displayName; TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef< const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>, React.ElementRef<typeof TabsPrimitive.Trigger>,
@@ -27,13 +27,13 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
ref={ref} ref={ref}
className={cn( 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, className,
)} )}
{...props} {...props}
/> />
)); ))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef< const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>, React.ElementRef<typeof TabsPrimitive.Content>,
@@ -42,12 +42,12 @@ const TabsContent = React.forwardRef<
<TabsPrimitive.Content <TabsPrimitive.Content
ref={ref} ref={ref}
className={cn( 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, className,
)} )}
{...props} {...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 TooltipPrimitive from '@radix-ui/react-tooltip'
import * as React from "react"; 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< const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>, React.ElementRef<typeof TooltipPrimitive.Content>,
@@ -19,12 +19,12 @@ const TooltipContent = React.forwardRef<
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( 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, className,
)} )}
{...props} {...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 // Ghost API configuration
const ghostConfig = { const ghostConfig = {
url: process.env.GHOST_URL || "https://site.com", url: process.env.GHOST_URL || 'https://site.com',
key: process.env.GHOST_KEY || "42424242424242424242424242424242", key: process.env.GHOST_KEY || '42424242424242424242424242424242',
version: "v5.0", version: 'v5.0',
}; }
// Initialize the Ghost API with your credentials // Initialize the Ghost API with your credentials
const api = GhostContentAPI({ const api = GhostContentAPI({
@@ -14,80 +14,80 @@ const api = GhostContentAPI({
version: ghostConfig.version, version: ghostConfig.version,
// @ts-ignore // @ts-ignore
makeRequest: ({ url, method, params, headers }) => { makeRequest: ({ url, method, params, headers }) => {
const apiUrl = new URL(url); const apiUrl = new URL(url)
// @ts-ignore // @ts-ignore
Object.keys(params).map((key) => Object.keys(params).map((key) =>
apiUrl.searchParams.set(key, encodeURIComponent(params[key])), apiUrl.searchParams.set(key, encodeURIComponent(params[key])),
); )
return fetch(apiUrl.toString(), { method, headers }) return fetch(apiUrl.toString(), { method, headers })
.then(async (res) => { .then(async (res) => {
// Check if the response was successful. // Check if the response was successful.
if (!res.ok) { if (!res.ok) {
// You can handle HTTP errors here // 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) => { .catch((error) => {
console.error("Fetch error:", error); console.error('Fetch error:', error)
}); })
}, },
}); })
export interface Post { export interface Post {
id: string; id: string
uuid: string; uuid: string
title: string; title: string
slug: string; slug: string
html: string; html: string
feature_image: string | null; feature_image: string | null
featured: boolean; featured: boolean
visibility: string; visibility: string
created_at: string; created_at: string
updated_at: string; updated_at: string
published_at: string; published_at: string
custom_excerpt: string | null; custom_excerpt: string | null
excerpt: string; excerpt: string
reading_time: number; reading_time: number
primary_tag?: { primary_tag?: {
id: string; id: string
name: string; name: string
slug: string; slug: string
}; }
tags?: Array<{ tags?: Array<{
id: string; id: string
name: string; name: string
slug: string; slug: string
}>; }>
primary_author?: { primary_author?: {
id: string; id: string
name: string; name: string
slug: string; slug: string
profile_image: string | null; profile_image: string | null
bio: string | null; bio: string | null
twitter: string | null; twitter: string | null
}; }
authors?: Array<{ authors?: Array<{
id: string; id: string
name: string; name: string
slug: string; slug: string
profile_image: string | null; profile_image: string | null
bio: string | null; bio: string | null
}>; }>
url: string; url: string
} }
export async function getPosts(options = {}): Promise<Post[]> { export async function getPosts(options = {}): Promise<Post[]> {
try { try {
const result = (await api.posts.browse({ const result = (await api.posts.browse({
include: "authors", include: 'authors',
limit: "all", limit: 'all',
})) as Post[]; })) as Post[]
return result; return result
} catch (error) { } catch (error) {
console.error("Error fetching posts:", error); console.error('Error fetching posts:', error)
return []; return []
} }
} }
@@ -95,36 +95,36 @@ export async function getPost(slug: string): Promise<Post | null> {
try { try {
const result = (await api.posts.read({ const result = (await api.posts.read({
slug, slug,
include: ["authors"], include: ['authors'],
})) as Post; })) as Post
return result; return result
} catch (error) { } catch (error) {
console.error("Error fetching post:", error); console.error('Error fetching post:', error)
return null; return null
} }
} }
export async function getTags() { export async function getTags() {
try { try {
const result = await api.tags.browse(); const result = await api.tags.browse()
return result; return result
} catch (error) { } catch (error) {
console.error("Error fetching tags:", error); console.error('Error fetching tags:', error)
return []; return []
} }
} }
export async function getPostsByTag(tag: string) { export async function getPostsByTag(tag: string) {
try { try {
const result = await api.posts.browse({ const result = await api.posts.browse({
limit: "all", limit: 'all',
filter: `tag:${tag}`, filter: `tag:${tag}`,
include: ["tags", "authors"], include: ['tags', 'authors'],
}); })
return result; return result
} catch (error) { } catch (error) {
console.error(`Error fetching posts with tag ${tag}:`, error); console.error(`Error fetching posts with tag ${tag}:`, error)
return []; 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>( export function useDebounce<T extends (...args: any[]) => any>(
callback: T, callback: T,
delay: number, delay: number,
): T { ): T {
const timeoutRef = useRef<NodeJS.Timeout>(); const timeoutRef = useRef<NodeJS.Timeout>()
useEffect(() => { useEffect(() => {
return () => { return () => {
if (timeoutRef.current) { if (timeoutRef.current) {
clearTimeout(timeoutRef.current); clearTimeout(timeoutRef.current)
} }
}; }
}, []); }, [])
return ((...args: Parameters<T>) => { return ((...args: Parameters<T>) => {
if (timeoutRef.current) { if (timeoutRef.current) {
clearTimeout(timeoutRef.current); clearTimeout(timeoutRef.current)
} }
return new Promise<ReturnType<T>>((resolve) => { return new Promise<ReturnType<T>>((resolve) => {
timeoutRef.current = setTimeout(() => { timeoutRef.current = setTimeout(() => {
resolve(callback(...args)); resolve(callback(...args))
}, delay); }, delay)
}); })
}) as T; }) as T
} }

View File

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

View File

@@ -1,48 +1,48 @@
import * as fs from "node:fs/promises"; import * as fs from 'node:fs/promises'
import { join } from "node:path"; import { join } from 'node:path'
import satori from "satori"; import satori from 'satori'
import sharp from "sharp"; import sharp from 'sharp'
interface GenerateOGImageOptions { interface GenerateOGImageOptions {
title: string; title: string
author?: { author?: {
name: string; name: string
image?: string; image?: string
}; }
date?: string; date?: string
readingTime?: number; readingTime?: number
} }
// Logo de Dokploy como SVG string (versión simplificada) // Logo de Dokploy como SVG string (versión simplificada)
const DOKPLOY_LOGO = { const DOKPLOY_LOGO = {
type: "svg", type: 'svg',
props: { props: {
width: "100%", width: '100%',
height: "100%", height: '100%',
viewBox: "0 0 6323 5778", viewBox: '0 0 6323 5778',
fill: "currentColor", fill: 'currentColor',
children: [ children: [
{ {
type: "path", type: 'path',
props: { 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: { 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: { 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({ export async function generateOGImage({
title, title,
@@ -52,122 +52,122 @@ export async function generateOGImage({
}: GenerateOGImageOptions): Promise<Buffer> { }: GenerateOGImageOptions): Promise<Buffer> {
// Cargar la fuente // Cargar la fuente
const interRegular = await fs.readFile( 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( 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 // Crear el markup para la imagen OG
const markup = { const markup = {
type: "div", type: 'div',
props: { props: {
style: { style: {
height: "100%", height: '100%',
width: "100%", width: '100%',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "flex-start", alignItems: 'flex-start',
justifyContent: "center", justifyContent: 'center',
backgroundColor: "#000000", backgroundColor: '#000000',
padding: "80px", padding: '80px',
position: "relative", position: 'relative',
overflow: "hidden", overflow: 'hidden',
}, },
children: [ children: [
{ {
type: "div", type: 'div',
props: { props: {
style: { style: {
position: "absolute", position: 'absolute',
left: "80px", left: '80px',
top: "40px", top: '40px',
fontSize: "32px", fontSize: '32px',
fontWeight: 700, fontWeight: 700,
color: "#fff", color: '#fff',
zIndex: 1, zIndex: 1,
}, },
children: "Dokploy - Blog Post", children: 'Dokploy - Blog Post',
}, },
}, },
{ {
type: "div", type: 'div',
props: { props: {
style: { style: {
position: "absolute", position: 'absolute',
right: "-50px", right: '-50px',
bottom: "-50px", bottom: '-50px',
width: "500px", width: '500px',
height: "500px", height: '500px',
opacity: 0.1, opacity: 0.1,
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
transform: "rotate(-10deg)", transform: 'rotate(-10deg)',
color: "#ffffff", color: '#ffffff',
}, },
children: DOKPLOY_LOGO, children: DOKPLOY_LOGO,
}, },
}, },
{ {
type: "div", type: 'div',
props: { props: {
style: { style: {
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: "24px", gap: '24px',
position: "relative", position: 'relative',
zIndex: 1, zIndex: 1,
}, },
children: [ children: [
{ {
type: "div", type: 'div',
props: { props: {
style: { style: {
fontSize: "64px", fontSize: '64px',
fontWeight: 700, fontWeight: 700,
color: "#fff", color: '#fff',
lineHeight: 1.2, lineHeight: 1.2,
maxWidth: "900px", maxWidth: '900px',
}, },
children: title, children: title,
}, },
}, },
{ {
type: "div", type: 'div',
props: { props: {
style: { style: {
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
gap: "16px", gap: '16px',
}, },
children: [ children: [
author?.name && { author?.name && {
type: "div", type: 'div',
props: { props: {
style: { style: {
color: "#9CA3AF", color: '#9CA3AF',
fontSize: "24px", fontSize: '24px',
}, },
children: author.name, children: author.name,
}, },
}, },
date && { date && {
type: "div", type: 'div',
props: { props: {
style: { style: {
color: "#9CA3AF", color: '#9CA3AF',
fontSize: "24px", fontSize: '24px',
}, },
children: `${date}`, children: `${date}`,
}, },
}, },
readingTime && { readingTime && {
type: "div", type: 'div',
props: { props: {
style: { style: {
color: "#9CA3AF", color: '#9CA3AF',
fontSize: "24px", fontSize: '24px',
}, },
children: `${readingTime} min read`, children: `${readingTime} min read`,
}, },
@@ -180,7 +180,7 @@ export async function generateOGImage({
}, },
], ],
}, },
}; }
// Generar SVG con Satori // Generar SVG con Satori
const svg = await satori(markup as any, { const svg = await satori(markup as any, {
@@ -188,22 +188,22 @@ export async function generateOGImage({
height: 630, height: 630,
fonts: [ fonts: [
{ {
name: "Inter", name: 'Inter',
data: interRegular, data: interRegular,
weight: 400, weight: 400,
style: "normal", style: 'normal',
}, },
{ {
name: "Inter", name: 'Inter',
data: interBold, data: interBold,
weight: 700, weight: 700,
style: "normal", style: 'normal',
}, },
], ],
}); })
// Convertir SVG a PNG // 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 { interface GhostContentAPIOptions {
url: string; url: string
key: string; key: string
version: string; version: string
} }
interface BrowseOptions { interface BrowseOptions {
limit?: string | number; limit?: string | number
page?: number; page?: number
order?: string; order?: string
filter?: string; filter?: string
include?: string | string[]; include?: string | string[]
fields?: string | string[]; fields?: string | string[]
formats?: string | string[]; formats?: string | string[]
} }
interface ReadOptions { interface ReadOptions {
id?: string; id?: string
slug?: string; slug?: string
include?: string | string[]; include?: string | string[]
fields?: string | string[]; fields?: string | string[]
formats?: string | string[]; formats?: string | string[]
} }
interface ApiObject { interface ApiObject {
browse<T>(options?: BrowseOptions): Promise<T[]>; browse<T>(options?: BrowseOptions): Promise<T[]>
read<T>(options: ReadOptions): Promise<T>; read<T>(options: ReadOptions): Promise<T>
} }
interface GhostAPI { interface GhostAPI {
posts: ApiObject; posts: ApiObject
tags: ApiObject; tags: ApiObject
authors: ApiObject; authors: ApiObject
pages: ApiObject; pages: ApiObject
settings: { 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 { type ClassValue, clsx } from 'clsx'
import { twMerge } from "tailwind-merge"; import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs))
} }

View File

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