mirror of
https://github.com/Dokploy/website.git
synced 2026-06-15 20:25:25 +02:00
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:
@@ -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.
|
||||||
|
|||||||
@@ -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",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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];
|
||||||
}),
|
}),
|
||||||
)),
|
)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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=""
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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 },
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 },
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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}</>
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 "<";
|
return '<'
|
||||||
case ">":
|
case '>':
|
||||||
return ">";
|
return '>'
|
||||||
case "&":
|
case '&':
|
||||||
return "&";
|
return '&'
|
||||||
case "'":
|
case "'":
|
||||||
return "'";
|
return '''
|
||||||
case '"':
|
case '"':
|
||||||
return """;
|
return '"'
|
||||||
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',
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
})),
|
})),
|
||||||
];
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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. It’s a pleasure to contribute to such an awesome project!",
|
body: 'This app convinced me to try something beyond pure Docker Compose. It’s 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. It’s stable, easy to use, and offers excellent support!",
|
body: 'I replaced my previous setup with Dokploy today. It’s 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. I’ve restructured my dev process around Dokploy!",
|
body: 'This is exactly what I want in a deployment system. I’ve 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. I’m running a mix of critical and low-priority services seamlessly across servers.",
|
body: 'Dokploy is a complete joy to use. I’m 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 we’re bragging? Hear from the devs who once doubted too—until
|
Think we’re bragging? Hear from the devs who once doubted
|
||||||
Dokploy made their lives (and deployments) surprisingly easier.
|
too—until 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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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">·</span>
|
<span aria-hidden="true">·</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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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 management—all with Dokploy’s developer-focused
|
and flexible database management—all with Dokploy’s
|
||||||
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>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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
@@ -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 backups—all without the fuss over minute details.
|
Simplify your project and data management, ensure robust
|
||||||
|
monitoring, and secure your backups—all 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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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 people—who knew?
|
this up. Turns out, Dokploy has actually helped a few
|
||||||
|
people—who 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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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 []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
52
apps/website/lib/types/ghost-content-api.d.ts
vendored
52
apps/website/lib/types/ghost-content-api.d.ts
vendored
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user