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
|
||||
```
|
||||
|
||||
|
||||
## Pull Request
|
||||
|
||||
- The `main` branch is the source of truth and should always reflect the latest stable release.
|
||||
|
||||
@@ -10,7 +10,9 @@ const inter = Inter({
|
||||
export default async function Layout({
|
||||
children,
|
||||
...rest
|
||||
}: { children: ReactNode }) {
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className={inter.className} suppressHydrationWarning>
|
||||
<body className="flex flex-col min-h-screen">
|
||||
|
||||
@@ -19,17 +19,20 @@ Open http://localhost:3000 with your browser to see the result.
|
||||
## Environment Variables
|
||||
|
||||
### Required for Contact Form
|
||||
|
||||
```
|
||||
RESEND_API_KEY=your_resend_api_key_here
|
||||
```
|
||||
|
||||
### Required for HubSpot Integration (Sales Forms)
|
||||
|
||||
```
|
||||
HUBSPOT_PORTAL_ID=147033433
|
||||
HUBSPOT_FORM_GUID=0d788925-ef54-4fda-9b76-741fb5877056
|
||||
```
|
||||
|
||||
### Required for Blog Page
|
||||
|
||||
```
|
||||
GHOST_URL=""
|
||||
GHOST_KEY=""
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { Resend } from "resend";
|
||||
import { submitToHubSpot, getHubSpotUTK } from "@/lib/hubspot";
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { Resend } from 'resend'
|
||||
import { submitToHubSpot, getHubSpotUTK } from '@/lib/hubspot'
|
||||
|
||||
interface ContactFormData {
|
||||
inquiryType: "support" | "sales" | "other";
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
company: string;
|
||||
message: string;
|
||||
inquiryType: 'support' | 'sales' | 'other'
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
company: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Initialize Resend with API key check
|
||||
const apiKey = process.env.RESEND_API_KEY;
|
||||
const apiKey = process.env.RESEND_API_KEY
|
||||
if (!apiKey) {
|
||||
console.error("RESEND_API_KEY is not configured");
|
||||
console.error('RESEND_API_KEY is not configured')
|
||||
return NextResponse.json(
|
||||
{ error: "Email service not configured" },
|
||||
{ error: 'Email service not configured' },
|
||||
{ status: 500 },
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const resend = new Resend(apiKey);
|
||||
const body: ContactFormData = await request.json();
|
||||
const resend = new Resend(apiKey)
|
||||
const body: ContactFormData = await request.json()
|
||||
|
||||
// Validate required fields
|
||||
if (
|
||||
@@ -37,41 +37,45 @@ export async function POST(request: NextRequest) {
|
||||
!body.message
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: "All fields are required" },
|
||||
{ error: 'All fields are required' },
|
||||
{ status: 400 },
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
if (!emailRegex.test(body.email)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid email format" },
|
||||
{ error: 'Invalid email format' },
|
||||
{ status: 400 },
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
// Submit to HubSpot if it's a sales inquiry
|
||||
if (body.inquiryType === "sales") {
|
||||
if (body.inquiryType === 'sales') {
|
||||
try {
|
||||
const hutk = getHubSpotUTK(request.headers.get("cookie") || undefined);
|
||||
const hubspotSuccess = await submitToHubSpot(body, hutk);
|
||||
const hutk = getHubSpotUTK(
|
||||
request.headers.get('cookie') || undefined,
|
||||
)
|
||||
const hubspotSuccess = await submitToHubSpot(body, hutk)
|
||||
|
||||
if (hubspotSuccess) {
|
||||
console.log("Successfully submitted sales inquiry to HubSpot");
|
||||
console.log(
|
||||
'Successfully submitted sales inquiry to HubSpot',
|
||||
)
|
||||
} else {
|
||||
console.warn(
|
||||
"Failed to submit sales inquiry to HubSpot, but continuing with email",
|
||||
);
|
||||
'Failed to submit sales inquiry to HubSpot, but continuing with email',
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error submitting to HubSpot:", error);
|
||||
console.error('Error submitting to HubSpot:', error)
|
||||
// Continue with email even if HubSpot fails
|
||||
}
|
||||
}
|
||||
|
||||
// Format email content
|
||||
const emailSubject = `[${body.inquiryType.toUpperCase()}] New contact form submission from ${body.firstName} ${body.lastName}`;
|
||||
const emailSubject = `[${body.inquiryType.toUpperCase()}] New contact form submission from ${body.firstName} ${body.lastName}`
|
||||
const emailBody = `
|
||||
New contact form submission:
|
||||
|
||||
@@ -86,23 +90,23 @@ ${body.message}
|
||||
|
||||
---
|
||||
Sent from Dokploy website contact form
|
||||
`.trim();
|
||||
`.trim()
|
||||
|
||||
// Send email to Dokploy team
|
||||
await resend.emails.send({
|
||||
from: "Dokploy Contact Form <noreply@emails.dokploy.com>",
|
||||
from: 'Dokploy Contact Form <noreply@emails.dokploy.com>',
|
||||
to:
|
||||
body.inquiryType === "sales"
|
||||
? ["sales@dokploy.com", "contact@dokploy.com"]
|
||||
: ["contact@dokploy.com"],
|
||||
body.inquiryType === 'sales'
|
||||
? ['sales@dokploy.com', 'contact@dokploy.com']
|
||||
: ['contact@dokploy.com'],
|
||||
subject: emailSubject,
|
||||
text: emailBody,
|
||||
replyTo: body.email,
|
||||
});
|
||||
})
|
||||
|
||||
// Send confirmation email to the user
|
||||
const confirmationSubject =
|
||||
"Thank you for contacting Dokploy - We received your message";
|
||||
'Thank you for contacting Dokploy - We received your message'
|
||||
const confirmationBody = `
|
||||
Hello ${body.firstName} ${body.lastName},
|
||||
|
||||
@@ -122,24 +126,24 @@ The Dokploy Team
|
||||
---
|
||||
This is an automated confirmation email. Please do not reply to this email.
|
||||
If you need immediate assistance, contact us at contact@dokploy.com
|
||||
`.trim();
|
||||
`.trim()
|
||||
|
||||
await resend.emails.send({
|
||||
from: "Dokploy Team <noreply@emails.dokploy.com>",
|
||||
from: 'Dokploy Team <noreply@emails.dokploy.com>',
|
||||
to: [body.email],
|
||||
subject: confirmationSubject,
|
||||
text: confirmationBody,
|
||||
});
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: "Contact form submitted successfully" },
|
||||
{ message: 'Contact form submitted successfully' },
|
||||
{ status: 200 },
|
||||
);
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error processing contact form:", error);
|
||||
console.error('Error processing contact form:', error)
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 },
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
// Cache the result for 5 minutes to avoid rate limiting
|
||||
let cachedStars: { count: number; timestamp: number } | null = null;
|
||||
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
|
||||
let cachedStars: { count: number; timestamp: number } | null = null
|
||||
const CACHE_DURATION = 5 * 60 * 1000 // 5 minutes in milliseconds
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const owner = searchParams.get("owner");
|
||||
const repo = searchParams.get("repo");
|
||||
const { searchParams } = new URL(request.url)
|
||||
const owner = searchParams.get('owner')
|
||||
const repo = searchParams.get('repo')
|
||||
|
||||
if (!owner || !repo) {
|
||||
return NextResponse.json(
|
||||
{ error: "Owner and repo parameters are required" },
|
||||
{ error: 'Owner and repo parameters are required' },
|
||||
{ status: 400 },
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
// Check if we have a valid cached result
|
||||
if (
|
||||
cachedStars &&
|
||||
Date.now() - cachedStars.timestamp < CACHE_DURATION
|
||||
) {
|
||||
if (cachedStars && Date.now() - cachedStars.timestamp < CACHE_DURATION) {
|
||||
return NextResponse.json(
|
||||
{ stargazers_count: cachedStars.count },
|
||||
{
|
||||
headers: {
|
||||
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
|
||||
'Cache-Control':
|
||||
'public, s-maxage=300, stale-while-revalidate=600',
|
||||
},
|
||||
},
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -36,42 +34,42 @@ export async function GET(request: Request) {
|
||||
`https://api.github.com/repos/${owner}/${repo}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "Dokploy-Website",
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'Dokploy-Website',
|
||||
},
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch repository data" },
|
||||
{ error: 'Failed to fetch repository data' },
|
||||
{ status: response.status },
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const starCount = data.stargazers_count;
|
||||
const data = await response.json()
|
||||
const starCount = data.stargazers_count
|
||||
|
||||
// Cache the result
|
||||
cachedStars = {
|
||||
count: starCount,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ stargazers_count: starCount },
|
||||
{
|
||||
headers: {
|
||||
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
|
||||
'Cache-Control':
|
||||
'public, s-maxage=300, stale-while-revalidate=600',
|
||||
},
|
||||
},
|
||||
);
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error fetching GitHub stars:", error);
|
||||
console.error('Error fetching GitHub stars:', error)
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 },
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
import { getPost } from "@/lib/ghost";
|
||||
import { generateOGImage } from "@/lib/og-image";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { getPost } from '@/lib/ghost'
|
||||
import { generateOGImage } from '@/lib/og-image'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const slug = searchParams.get("slug");
|
||||
const { searchParams } = new URL(request.url)
|
||||
const slug = searchParams.get('slug')
|
||||
|
||||
console.log("Generating OG image for slug:", slug);
|
||||
console.log('Generating OG image for slug:', slug)
|
||||
|
||||
if (!slug) {
|
||||
console.error("Missing slug parameter");
|
||||
return new Response("Missing slug parameter", { status: 400 });
|
||||
console.error('Missing slug parameter')
|
||||
return new Response('Missing slug parameter', { status: 400 })
|
||||
}
|
||||
|
||||
const post = await getPost(slug);
|
||||
const post = await getPost(slug)
|
||||
|
||||
if (!post) {
|
||||
console.error("Post not found for slug:", slug);
|
||||
return new Response("Post not found", { status: 404 });
|
||||
console.error('Post not found for slug:', slug)
|
||||
return new Response('Post not found', { status: 404 })
|
||||
}
|
||||
|
||||
|
||||
const formattedDate = new Date(post.published_at).toLocaleDateString(
|
||||
"en-US",
|
||||
'en-US',
|
||||
{
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
const ogImage = await generateOGImage({
|
||||
title: post.title,
|
||||
@@ -41,16 +40,16 @@ export async function GET(request: NextRequest) {
|
||||
: undefined,
|
||||
date: formattedDate,
|
||||
readingTime: post.reading_time,
|
||||
});
|
||||
})
|
||||
|
||||
return new Response(ogImage, {
|
||||
headers: {
|
||||
"Content-Type": "image/png",
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
'Content-Type': 'image/png',
|
||||
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||
},
|
||||
});
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error generating OG image:", error);
|
||||
return new Response(`Error generating image: ${error}`, { status: 500 });
|
||||
console.error('Error generating OG image:', error)
|
||||
return new Response(`Error generating image: ${error}`, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { CopyButton } from "@/components/ui/copy-button";
|
||||
import * as babel from "prettier/plugins/babel";
|
||||
import * as estree from "prettier/plugins/estree";
|
||||
import * as yaml from "prettier/plugins/yaml";
|
||||
import * as prettier from "prettier/standalone";
|
||||
import { type JSX, useLayoutEffect, useState } from "react";
|
||||
import type { BundledLanguage } from "shiki/bundle/web";
|
||||
import { highlight } from "./shared";
|
||||
import { CopyButton } from '@/components/ui/copy-button'
|
||||
import * as babel from 'prettier/plugins/babel'
|
||||
import * as estree from 'prettier/plugins/estree'
|
||||
import * as yaml from 'prettier/plugins/yaml'
|
||||
import * as prettier from 'prettier/standalone'
|
||||
import { type JSX, useLayoutEffect, useState } from 'react'
|
||||
import type { BundledLanguage } from 'shiki/bundle/web'
|
||||
import { highlight } from './shared'
|
||||
|
||||
interface CodeBlockProps {
|
||||
code: string;
|
||||
lang: BundledLanguage;
|
||||
initial?: JSX.Element;
|
||||
code: string
|
||||
lang: BundledLanguage
|
||||
initial?: JSX.Element
|
||||
}
|
||||
|
||||
async function formatCode(code: string, lang: string) {
|
||||
try {
|
||||
let parser: string;
|
||||
let plugins = [] as any[];
|
||||
let parser: string
|
||||
let plugins = [] as any[]
|
||||
switch (lang.toLowerCase()) {
|
||||
case "yaml":
|
||||
case "yml":
|
||||
parser = "yaml";
|
||||
plugins = [yaml];
|
||||
break;
|
||||
case "javascript":
|
||||
case "typescript":
|
||||
case "jsx":
|
||||
case "tsx":
|
||||
parser = "babel-ts";
|
||||
plugins = [babel, estree];
|
||||
break;
|
||||
case 'yaml':
|
||||
case 'yml':
|
||||
parser = 'yaml'
|
||||
plugins = [yaml]
|
||||
break
|
||||
case 'javascript':
|
||||
case 'typescript':
|
||||
case 'jsx':
|
||||
case 'tsx':
|
||||
parser = 'babel-ts'
|
||||
plugins = [babel, estree]
|
||||
break
|
||||
default:
|
||||
return code;
|
||||
return code
|
||||
}
|
||||
const formatted = await prettier.format(code, {
|
||||
parser,
|
||||
@@ -43,50 +43,50 @@ async function formatCode(code: string, lang: string) {
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
printWidth: 120,
|
||||
});
|
||||
return formatted;
|
||||
})
|
||||
return formatted
|
||||
} catch (error) {
|
||||
console.error("Error formatting code:", error);
|
||||
return code;
|
||||
console.error('Error formatting code:', error)
|
||||
return code
|
||||
}
|
||||
}
|
||||
|
||||
export function CodeBlock({ code, lang, initial }: CodeBlockProps) {
|
||||
const [nodes, setNodes] = useState<JSX.Element | undefined>(initial);
|
||||
const [formattedCode, setFormattedCode] = useState(code);
|
||||
const [nodes, setNodes] = useState<JSX.Element | undefined>(initial)
|
||||
const [formattedCode, setFormattedCode] = useState(code)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
async function formatAndHighlight() {
|
||||
try {
|
||||
const formatted = await formatCode(code, lang);
|
||||
setFormattedCode(formatted);
|
||||
const highlighted = await highlight(formatted, lang);
|
||||
setNodes(highlighted);
|
||||
const formatted = await formatCode(code, lang)
|
||||
setFormattedCode(formatted)
|
||||
const highlighted = await highlight(formatted, lang)
|
||||
setNodes(highlighted)
|
||||
} catch (error) {
|
||||
const highlighted = await highlight(code, lang);
|
||||
setNodes(highlighted);
|
||||
const highlighted = await highlight(code, lang)
|
||||
setNodes(highlighted)
|
||||
}
|
||||
}
|
||||
void formatAndHighlight();
|
||||
}, [code, lang]);
|
||||
void formatAndHighlight()
|
||||
}, [code, lang])
|
||||
|
||||
if (!nodes) {
|
||||
return (
|
||||
<div className="group relative">
|
||||
<div className="text-sm p-4 rounded-lg bg-[#18191F] overflow-auto animate-pulse">
|
||||
<div className="h-4 bg-gray-700 rounded w-3/4 mb-2" />
|
||||
<div className="h-4 bg-gray-700 rounded w-1/2" />
|
||||
<div className="animate-pulse overflow-auto rounded-lg bg-[#18191F] p-4 text-sm">
|
||||
<div className="mb-2 h-4 w-3/4 rounded bg-gray-700" />
|
||||
<div className="h-4 w-1/2 rounded bg-gray-700" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group relative">
|
||||
<CopyButton text={formattedCode} />
|
||||
<div className="text-sm p-4 rounded-lg bg-[#18191F] overflow-auto">
|
||||
<div className="overflow-auto rounded-lg bg-[#18191F] p-4 text-sm">
|
||||
{nodes}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { DetailedHTMLProps, HTMLAttributes } from "react";
|
||||
import slugify from "slugify";
|
||||
import { useRouter } from 'next/navigation'
|
||||
import type { DetailedHTMLProps, HTMLAttributes } from 'react'
|
||||
import slugify from 'slugify'
|
||||
|
||||
type HeadingProps = DetailedHTMLProps<
|
||||
HTMLAttributes<HTMLHeadingElement>,
|
||||
HTMLHeadingElement
|
||||
>;
|
||||
>
|
||||
|
||||
function LinkIcon() {
|
||||
return (
|
||||
<svg
|
||||
className="inline-block w-4 h-4 ml-2 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
className="ml-2 inline-block h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -24,62 +24,71 @@ function LinkIcon() {
|
||||
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export function H1({ children, ...props }: HeadingProps) {
|
||||
const router = useRouter();
|
||||
const id = slugify(children?.toString() || "", { lower: true, strict: true });
|
||||
const router = useRouter()
|
||||
const id = slugify(children?.toString() || '', {
|
||||
lower: true,
|
||||
strict: true,
|
||||
})
|
||||
const handleClick = () => {
|
||||
router.push(`#${id}`);
|
||||
};
|
||||
router.push(`#${id}`)
|
||||
}
|
||||
return (
|
||||
<h1
|
||||
id={id}
|
||||
onClick={handleClick}
|
||||
className="group text-xl md:text-2xl xl:text-3xl text-primary font-bold mt-8 mb-4 cursor-pointer hover:text-primary/80 transition-colors"
|
||||
className="group mb-4 mt-8 cursor-pointer text-xl font-bold text-primary transition-colors hover:text-primary/80 md:text-2xl xl:text-3xl"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<LinkIcon />
|
||||
</h1>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export function H2({ children, ...props }: HeadingProps) {
|
||||
const router = useRouter();
|
||||
const id = slugify(children?.toString() || "", { lower: true, strict: true });
|
||||
const router = useRouter()
|
||||
const id = slugify(children?.toString() || '', {
|
||||
lower: true,
|
||||
strict: true,
|
||||
})
|
||||
const handleClick = () => {
|
||||
router.push(`#${id}`);
|
||||
};
|
||||
router.push(`#${id}`)
|
||||
}
|
||||
return (
|
||||
<h2
|
||||
id={id}
|
||||
onClick={handleClick}
|
||||
className="group text-2xl text-primary/90 font-semibold mt-6 mb-3 cursor-pointer hover:text-primary/80 transition-colors"
|
||||
className="group mb-3 mt-6 cursor-pointer text-2xl font-semibold text-primary/90 transition-colors hover:text-primary/80"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<LinkIcon />
|
||||
</h2>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export function H3({ children, ...props }: HeadingProps) {
|
||||
const router = useRouter();
|
||||
const id = slugify(children?.toString() || "", { lower: true, strict: true });
|
||||
const router = useRouter()
|
||||
const id = slugify(children?.toString() || '', {
|
||||
lower: true,
|
||||
strict: true,
|
||||
})
|
||||
const handleClick = () => {
|
||||
router.push(`#${id}`);
|
||||
};
|
||||
router.push(`#${id}`)
|
||||
}
|
||||
return (
|
||||
<h3
|
||||
id={id}
|
||||
onClick={handleClick}
|
||||
className="group text-xl text-primary/90 font-semibold mt-4 mb-2 cursor-pointer hover:text-primary/80 transition-colors"
|
||||
className="group mb-2 mt-4 cursor-pointer text-xl font-semibold text-primary/90 transition-colors hover:text-primary/80"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<LinkIcon />
|
||||
</h3>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,65 +1,67 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface Heading {
|
||||
id: string;
|
||||
text: string;
|
||||
level: number;
|
||||
id: string
|
||||
text: string
|
||||
level: number
|
||||
}
|
||||
|
||||
export function TableOfContents() {
|
||||
const [headings, setHeadings] = useState<Heading[]>([]);
|
||||
const [activeId, setActiveId] = useState<string>();
|
||||
const [headings, setHeadings] = useState<Heading[]>([])
|
||||
const [activeId, setActiveId] = useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
const elements = Array.from(document.querySelectorAll("h1, h2, h3"))
|
||||
const elements = Array.from(document.querySelectorAll('h1, h2, h3'))
|
||||
.filter((element) => element.id)
|
||||
.map((element) => ({
|
||||
id: element.id,
|
||||
text: element.textContent || "",
|
||||
text: element.textContent || '',
|
||||
level: Number(element.tagName.charAt(1)),
|
||||
}));
|
||||
setHeadings(elements);
|
||||
}))
|
||||
setHeadings(elements)
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
setActiveId(entry.target.id);
|
||||
setActiveId(entry.target.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ rootMargin: "-100px 0px -66%" },
|
||||
);
|
||||
{ rootMargin: '-100px 0px -66%' },
|
||||
)
|
||||
|
||||
for (const { id } of elements) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) observer.observe(element);
|
||||
const element = document.getElementById(id)
|
||||
if (element) observer.observe(element)
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<nav className="space-y-2 text-sm">
|
||||
<p className="font-medium mb-4">Table of Contents</p>
|
||||
<p className="mb-4 font-medium">Table of Contents</p>
|
||||
<ul className="space-y-2">
|
||||
{headings.length > 0 ? (
|
||||
headings.map((heading) => (
|
||||
<li
|
||||
key={heading.id}
|
||||
style={{ paddingLeft: `${(heading.level - 1) * 1}rem` }}
|
||||
style={{
|
||||
paddingLeft: `${(heading.level - 1) * 1}rem`,
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href={`#${heading.id}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
document
|
||||
.getElementById(heading.id)
|
||||
?.scrollIntoView({ behavior: "smooth" });
|
||||
?.scrollIntoView({ behavior: 'smooth' })
|
||||
}}
|
||||
className={`hover:text-primary transition-colors block ${activeId === heading.id ? "text-primary font-medium" : "text-muted-foreground"}`}
|
||||
className={`block transition-colors hover:text-primary ${activeId === heading.id ? 'font-medium text-primary' : 'text-muted-foreground'}`}
|
||||
>
|
||||
{heading.text}
|
||||
</a>
|
||||
@@ -67,10 +69,12 @@ export function TableOfContents() {
|
||||
))
|
||||
) : (
|
||||
<li>
|
||||
<p className="text-muted-foreground">No headings found</p>
|
||||
<p className="text-muted-foreground">
|
||||
No headings found
|
||||
</p>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PhotoProvider, PhotoView } from "react-photo-view";
|
||||
import "react-photo-view/dist/react-photo-view.css";
|
||||
import { cn } from '@/lib/utils'
|
||||
import { PhotoProvider, PhotoView } from 'react-photo-view'
|
||||
import 'react-photo-view/dist/react-photo-view.css'
|
||||
|
||||
interface ZoomableImageProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
className?: string;
|
||||
src: string
|
||||
alt: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function ZoomableImage({ src, alt, className }: ZoomableImageProps) {
|
||||
return (
|
||||
<PhotoProvider>
|
||||
<PhotoView src={src}>
|
||||
<img src={src} alt={alt} className={cn("object-cover", className)} />
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className={cn('object-cover', className)}
|
||||
/>
|
||||
</PhotoView>
|
||||
</PhotoProvider>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime";
|
||||
import type { JSX } from "react";
|
||||
import { Fragment } from "react";
|
||||
import { jsx, jsxs } from "react/jsx-runtime";
|
||||
import type { BundledLanguage } from "shiki/bundle/web";
|
||||
import { codeToHast } from "shiki/bundle/web";
|
||||
import { toJsxRuntime } from 'hast-util-to-jsx-runtime'
|
||||
import type { JSX } from 'react'
|
||||
import { Fragment } from 'react'
|
||||
import { jsx, jsxs } from 'react/jsx-runtime'
|
||||
import type { BundledLanguage } from 'shiki/bundle/web'
|
||||
import { codeToHast } from 'shiki/bundle/web'
|
||||
|
||||
export async function highlight(code: string, lang: BundledLanguage) {
|
||||
const out = await codeToHast(code, {
|
||||
lang,
|
||||
theme: "houston",
|
||||
});
|
||||
return toJsxRuntime(out, { Fragment, jsx, jsxs }) as JSX.Element;
|
||||
theme: 'houston',
|
||||
})
|
||||
return toJsxRuntime(out, { Fragment, jsx, jsxs }) as JSX.Element
|
||||
}
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
import { getPost, getPosts } from "@/lib/ghost";
|
||||
import type { Metadata, ResolvingMetadata } from "next";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import type React from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import type { Components } from "react-markdown";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkToc from "remark-toc";
|
||||
import type { BundledLanguage } from "shiki/bundle/web";
|
||||
import TurndownService from "turndown";
|
||||
import { getPost, getPosts } from '@/lib/ghost'
|
||||
import type { Metadata, ResolvingMetadata } from 'next'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
import type React from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import type { Components } from 'react-markdown'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkToc from 'remark-toc'
|
||||
import type { BundledLanguage } from 'shiki/bundle/web'
|
||||
import TurndownService from 'turndown'
|
||||
// @ts-ignore
|
||||
import * as turndownPluginGfm from "turndown-plugin-gfm";
|
||||
import { CodeBlock } from "./components/CodeBlock";
|
||||
import { H1, H2, H3 } from "./components/Headings";
|
||||
import { TableOfContents } from "./components/TableOfContents";
|
||||
import { ZoomableImage } from "./components/ZoomableImage";
|
||||
import * as turndownPluginGfm from 'turndown-plugin-gfm'
|
||||
import { CodeBlock } from './components/CodeBlock'
|
||||
import { H1, H2, H3 } from './components/Headings'
|
||||
import { TableOfContents } from './components/TableOfContents'
|
||||
import { ZoomableImage } from './components/ZoomableImage'
|
||||
|
||||
type Props = {
|
||||
params: { slug: string };
|
||||
};
|
||||
params: { slug: string }
|
||||
}
|
||||
|
||||
export async function generateMetadata(
|
||||
{ params }: Props,
|
||||
parent: ResolvingMetadata,
|
||||
): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const post = await getPost(slug);
|
||||
const { slug } = await params
|
||||
const post = await getPost(slug)
|
||||
|
||||
if (!post) {
|
||||
return {
|
||||
title: "Post Not Found",
|
||||
};
|
||||
title: 'Post Not Found',
|
||||
}
|
||||
}
|
||||
|
||||
const ogUrl = new URL(
|
||||
`/api/og`,
|
||||
process.env.NODE_ENV === "production"
|
||||
? "https://dokploy.com"
|
||||
: "http://localhost:3000",
|
||||
);
|
||||
ogUrl.searchParams.set("slug", slug);
|
||||
process.env.NODE_ENV === 'production'
|
||||
? 'https://dokploy.com'
|
||||
: 'http://localhost:3000',
|
||||
)
|
||||
ogUrl.searchParams.set('slug', slug)
|
||||
|
||||
return {
|
||||
title: post.title,
|
||||
@@ -49,7 +49,7 @@ export async function generateMetadata(
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.custom_excerpt || post.excerpt,
|
||||
type: "article",
|
||||
type: 'article',
|
||||
url: `${process.env.NEXT_PUBLIC_APP_URL}/blog/${post.slug}`,
|
||||
images: [
|
||||
{
|
||||
@@ -61,66 +61,66 @@ export async function generateMetadata(
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
card: 'summary_large_image',
|
||||
title: post.title,
|
||||
description: post.custom_excerpt || post.excerpt,
|
||||
images: [ogUrl.toString()],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default async function BlogPostPage({ params }: Props) {
|
||||
const { slug } = await params;
|
||||
const post = await getPost(slug);
|
||||
const allPosts = await getPosts();
|
||||
const { slug } = await params
|
||||
const post = await getPost(slug)
|
||||
const allPosts = await getPosts()
|
||||
|
||||
const relatedPosts = allPosts.filter((p) => p.id !== post?.id).slice(0, 3);
|
||||
const relatedPosts = allPosts.filter((p) => p.id !== post?.id).slice(0, 3)
|
||||
|
||||
if (!post) {
|
||||
notFound();
|
||||
notFound()
|
||||
}
|
||||
|
||||
const cleanHtml = (html: string) => {
|
||||
if (typeof window !== "undefined") {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
if (typeof window !== 'undefined') {
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(html, 'text/html')
|
||||
const scripts = doc.querySelectorAll(
|
||||
'script[type="application/ld+json"], script',
|
||||
);
|
||||
scripts.forEach((script) => script.remove());
|
||||
const unwantedElements = doc.querySelectorAll("style, meta, link");
|
||||
unwantedElements.forEach((el) => el.remove());
|
||||
return doc.body.innerHTML;
|
||||
)
|
||||
scripts.forEach((script) => script.remove())
|
||||
const unwantedElements = doc.querySelectorAll('style, meta, link')
|
||||
unwantedElements.forEach((el) => el.remove())
|
||||
return doc.body.innerHTML
|
||||
} else {
|
||||
return html
|
||||
.replace(
|
||||
/<script[^>]*type="application\/ld\+json"[^>]*>[\s\S]*?<\/script>/gi,
|
||||
"",
|
||||
'',
|
||||
)
|
||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
|
||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
|
||||
.replace(/<meta[^>]*>/gi, "")
|
||||
.replace(/<link[^>]*>/gi, "");
|
||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||||
.replace(/<meta[^>]*>/gi, '')
|
||||
.replace(/<link[^>]*>/gi, '')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const turndownService = new TurndownService({
|
||||
headingStyle: "atx",
|
||||
codeBlockStyle: "fenced",
|
||||
});
|
||||
const gfm = turndownPluginGfm.gfm;
|
||||
const tables = turndownPluginGfm.tables;
|
||||
const strikethrough = turndownPluginGfm.strikethrough;
|
||||
turndownService.use([tables, strikethrough, gfm, remarkToc]);
|
||||
headingStyle: 'atx',
|
||||
codeBlockStyle: 'fenced',
|
||||
})
|
||||
const gfm = turndownPluginGfm.gfm
|
||||
const tables = turndownPluginGfm.tables
|
||||
const strikethrough = turndownPluginGfm.strikethrough
|
||||
turndownService.use([tables, strikethrough, gfm, remarkToc])
|
||||
|
||||
const cleanedHtml = cleanHtml(post.html);
|
||||
const markdown = turndownService.turndown(cleanedHtml);
|
||||
const cleanedHtml = cleanHtml(post.html)
|
||||
const markdown = turndownService.turndown(cleanedHtml)
|
||||
|
||||
const formattedDate = new Date(post.published_at).toLocaleDateString("en", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
const formattedDate = new Date(post.published_at).toLocaleDateString('en', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
|
||||
const components: Partial<Components> = {
|
||||
h1: H1,
|
||||
@@ -128,7 +128,7 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
h3: H3,
|
||||
p: ({ node, children, ...props }) => (
|
||||
<p
|
||||
className="text-base text-muted-foreground leading-relaxed mb-4"
|
||||
className="mb-4 text-base leading-relaxed text-muted-foreground"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
@@ -137,7 +137,7 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
a: ({ node, href, ...props }) => (
|
||||
<a
|
||||
href={href}
|
||||
className="text-blue-500 hover:text-blue-500/80 transition-colors"
|
||||
className="text-blue-500 transition-colors hover:text-blue-500/80"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{...props}
|
||||
@@ -145,32 +145,32 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
),
|
||||
ul: ({ node, ...props }) => (
|
||||
<ul
|
||||
className="list-disc pl-6 space-y-1 mb-4 text-muted-foreground"
|
||||
className="mb-4 list-disc space-y-1 pl-6 text-muted-foreground"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ol: ({ node, ...props }) => (
|
||||
<ol
|
||||
className="list-decimal pl-6 space-y-1 mb-4 text-muted-foreground"
|
||||
className="mb-4 list-decimal space-y-1 pl-6 text-muted-foreground"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
li: ({ node, ...props }) => (
|
||||
<li className="text-base leading-relaxed ml-2" {...props} />
|
||||
<li className="ml-2 text-base leading-relaxed" {...props} />
|
||||
),
|
||||
blockquote: ({ node, ...props }) => (
|
||||
<blockquote
|
||||
className="border-l-4 border-primary pl-4 py-2 my-4 bg-muted/50"
|
||||
className="my-4 border-l-4 border-primary bg-muted/50 py-2 pl-4"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
table: ({ node, ...props }) => (
|
||||
<div className="my-6 w-full overflow-x-auto border rounded-lg">
|
||||
<div className="my-6 w-full overflow-x-auto rounded-lg border">
|
||||
<table className="w-full border-collapse" {...props} />
|
||||
</div>
|
||||
),
|
||||
thead: ({ node, ...props }) => (
|
||||
<thead className="bg-muted border-b border-border" {...props} />
|
||||
<thead className="border-b border-border bg-muted" {...props} />
|
||||
),
|
||||
tbody: ({ node, ...props }) => (
|
||||
<tbody className="divide-y divide-border" {...props} />
|
||||
@@ -186,42 +186,46 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
),
|
||||
img: ({ node, src, alt }) => (
|
||||
<ZoomableImage
|
||||
src={src || ""}
|
||||
alt={alt || ""}
|
||||
className="object-cover max-w-lg mx-auto rounded-lg border max-lg:w-64 border-border overflow-hidden"
|
||||
src={src || ''}
|
||||
alt={alt || ''}
|
||||
className="mx-auto max-w-lg overflow-hidden rounded-lg border border-border object-cover max-lg:w-64"
|
||||
/>
|
||||
),
|
||||
code: ({
|
||||
className,
|
||||
children,
|
||||
inline,
|
||||
}: { className: string; children: React.ReactNode; inline: boolean }) => {
|
||||
}: {
|
||||
className: string
|
||||
children: React.ReactNode
|
||||
inline: boolean
|
||||
}) => {
|
||||
if (inline || !className || !/language-(\w+)/.test(className)) {
|
||||
return (
|
||||
<code className="px-1.5 py-0.5 bg-muted text-sm rounded font-mono text-foreground">
|
||||
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-sm text-foreground">
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
)
|
||||
}
|
||||
const match = /language-(\w+)/.exec(className);
|
||||
const match = /language-(\w+)/.exec(className)
|
||||
return (
|
||||
<CodeBlock
|
||||
lang={match ? (match[1] as BundledLanguage) : "ts"}
|
||||
code={children?.toString() || ""}
|
||||
lang={match ? (match[1] as BundledLanguage) : 'ts'}
|
||||
code={children?.toString() || ''}
|
||||
/>
|
||||
);
|
||||
)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="mx-auto px-4 sm:px-6 lg:px-8 pb-12 max-w-7xl w-full">
|
||||
<article className="mx-auto w-full max-w-7xl px-4 pb-12 sm:px-6 lg:px-8">
|
||||
<Link
|
||||
href="/blog"
|
||||
className="inline-flex items-center mb-8 text-primary hover:text-primary/80 transition-colors"
|
||||
className="mb-8 inline-flex items-center text-primary transition-colors hover:text-primary/80"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5 mr-2"
|
||||
className="mr-2 h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
@@ -234,15 +238,15 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
Back to Blog
|
||||
</Link>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[1fr_250px] gap-8">
|
||||
<div className="rounded-lg p-8 shadow-lg border border-border">
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-[1fr_250px]">
|
||||
<div className="rounded-lg border border-border p-8 shadow-lg">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-xl md:text-2xl xl:text-3xl font-bold mb-4">
|
||||
<h1 className="mb-4 text-xl font-bold md:text-2xl xl:text-3xl">
|
||||
{post.title}
|
||||
</h1>
|
||||
<div className="flex items-center mb-6">
|
||||
<div className="mb-6 flex items-center">
|
||||
{post.primary_author?.profile_image && (
|
||||
<div className="relative h-12 w-12 rounded-full overflow-hidden mr-4">
|
||||
<div className="relative mr-4 h-12 w-12 overflow-hidden rounded-full">
|
||||
{post.primary_author.twitter ? (
|
||||
<a
|
||||
href={`https://twitter.com/${post.primary_author.twitter}`}
|
||||
@@ -251,14 +255,20 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
className="block cursor-pointer transition-opacity hover:opacity-90"
|
||||
>
|
||||
<img
|
||||
src={post.primary_author.profile_image}
|
||||
src={
|
||||
post.primary_author
|
||||
.profile_image
|
||||
}
|
||||
alt={post.primary_author.name}
|
||||
className="object-cover"
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
<img
|
||||
src={post.primary_author.profile_image}
|
||||
src={
|
||||
post.primary_author
|
||||
.profile_image
|
||||
}
|
||||
alt={post.primary_author.name}
|
||||
className="object-cover"
|
||||
/>
|
||||
@@ -272,25 +282,28 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
href={`https://twitter.com/${post.primary_author.twitter}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-primary transition-colors"
|
||||
className="transition-colors hover:text-primary"
|
||||
>
|
||||
{post.primary_author.name || "Unknown Author"}
|
||||
{post.primary_author.name ||
|
||||
'Unknown Author'}
|
||||
</a>
|
||||
) : (
|
||||
post.primary_author?.name || "Unknown Author"
|
||||
post.primary_author?.name ||
|
||||
'Unknown Author'
|
||||
)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formattedDate} • {post.reading_time} min read
|
||||
{formattedDate} • {post.reading_time} min
|
||||
read
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{post.feature_image && (
|
||||
<div className="relative w-full h-[400px] mb-8">
|
||||
<div className="relative mb-8 h-[400px] w-full">
|
||||
<ZoomableImage
|
||||
src={post.feature_image}
|
||||
alt={post.title}
|
||||
className="rounded-lg h-full w-full object-cover"
|
||||
className="h-full w-full rounded-lg object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -310,14 +323,14 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
</div>
|
||||
|
||||
{post.tags && post.tags.length > 0 && (
|
||||
<div className="mt-12 pt-6 border-t border-border">
|
||||
<h2 className="text-xl font-semibold mb-4">Tags</h2>
|
||||
<div className="mt-12 border-t border-border pt-6">
|
||||
<h2 className="mb-4 text-xl font-semibold">Tags</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{post.tags.map((tag) => (
|
||||
<Link
|
||||
key={tag.id}
|
||||
href={`/blog/tag/${tag.slug}`}
|
||||
className="px-4 py-2 bg-muted hover:bg-muted/80 rounded-full text-sm transition-colors"
|
||||
className="rounded-full bg-muted px-4 py-2 text-sm transition-colors hover:bg-muted/80"
|
||||
>
|
||||
{tag.name}
|
||||
</Link>
|
||||
@@ -327,7 +340,7 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:block max-w-[16rem]">
|
||||
<div className="hidden max-w-[16rem] lg:block">
|
||||
<div className="sticky top-4">
|
||||
<TableOfContents />
|
||||
</div>
|
||||
@@ -336,16 +349,16 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
|
||||
{relatedPosts.length > 0 && (
|
||||
<div className="mt-12">
|
||||
<h2 className="text-2xl font-bold mb-6">Related Posts</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<h2 className="mb-6 text-2xl font-bold">Related Posts</h2>
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
{relatedPosts.map((relatedPost) => {
|
||||
const relatedPostDate = new Date(
|
||||
relatedPost.published_at,
|
||||
).toLocaleDateString("en", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
).toLocaleDateString('en', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
|
||||
return (
|
||||
<Link
|
||||
@@ -353,34 +366,39 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
href={`/blog/${relatedPost.slug}`}
|
||||
className="group"
|
||||
>
|
||||
<div className="bg-card rounded-lg overflow-hidden h-full shadow-lg transition-all duration-300 hover:shadow-xl border border-border">
|
||||
<div className="h-full overflow-hidden rounded-lg border border-border bg-card shadow-lg transition-all duration-300 hover:shadow-xl">
|
||||
{relatedPost.feature_image && (
|
||||
<div className="relative w-full">
|
||||
<img
|
||||
src={relatedPost.feature_image || "/og.png"}
|
||||
src={
|
||||
relatedPost.feature_image ||
|
||||
'/og.png'
|
||||
}
|
||||
alt={relatedPost.title}
|
||||
className="object-cover "
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-2 group-hover:text-primary transition-colors line-clamp-2">
|
||||
<h3 className="mb-2 line-clamp-2 text-lg font-semibold transition-colors group-hover:text-primary">
|
||||
{relatedPost.title}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{relatedPostDate} • {relatedPost.reading_time} min read
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
{relatedPostDate} •{' '}
|
||||
{relatedPost.reading_time} min
|
||||
read
|
||||
</p>
|
||||
<p className="text-muted-foreground line-clamp-2">
|
||||
<p className="line-clamp-2 text-muted-foreground">
|
||||
{relatedPost.excerpt}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import type { Post } from "@/lib/ghost";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { Post } from '@/lib/ghost'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
interface BlogPostCardProps {
|
||||
post: Post;
|
||||
post: Post
|
||||
}
|
||||
|
||||
export function BlogPostCard({ post }: BlogPostCardProps) {
|
||||
const router = useRouter();
|
||||
const formattedDate = new Date(post.published_at).toLocaleDateString("en", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
const router = useRouter()
|
||||
const formattedDate = new Date(post.published_at).toLocaleDateString('en', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
|
||||
const handleTwitterClick = (e: React.MouseEvent) => {
|
||||
if (post.primary_author?.twitter) {
|
||||
router.push(`https://twitter.com/${post.primary_author.twitter}`);
|
||||
router.push(`https://twitter.com/${post.primary_author.twitter}`)
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/blog/${post.slug}`}
|
||||
className="group block hover:bg-muted p-4 rounded-lg border border-border"
|
||||
className="group block rounded-lg border border-border p-4 hover:bg-muted"
|
||||
>
|
||||
<article className="flex gap-6 items-start max-sm:flex-col items-center">
|
||||
<div className="relative shrink-0 flex items-center justify-center mx-auto">
|
||||
<article className="flex items-start items-center gap-6 max-sm:flex-col">
|
||||
<div className="relative mx-auto flex shrink-0 items-center justify-center">
|
||||
<img
|
||||
src={post.feature_image || "/og.png"}
|
||||
alt={post.feature_image ? post.title : "Default Image"}
|
||||
className="object-cover rounded-lg object-center mx-auto self-center h-32 w-64 sm:w-32 sm:h-24"
|
||||
src={post.feature_image || '/og.png'}
|
||||
alt={post.feature_image ? post.title : 'Default Image'}
|
||||
className="mx-auto h-32 w-64 self-center rounded-lg object-cover object-center sm:h-24 sm:w-32"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full flex-wrap flex">
|
||||
<h2 className="text-xl font-semibold mb-2 group-hover:text-primary">
|
||||
<div className="flex w-full flex-wrap">
|
||||
<h2 className="mb-2 text-xl font-semibold group-hover:text-primary">
|
||||
{post.title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground line-clamp-2 mb-4">
|
||||
<p className="mb-4 line-clamp-2 text-muted-foreground">
|
||||
{post.custom_excerpt || post.excerpt}
|
||||
</p>
|
||||
<div className="flex items-center text-sm text-muted-foreground flex-wrap">
|
||||
<div className="flex flex-wrap items-center text-sm text-muted-foreground">
|
||||
<div className="flex items-center">
|
||||
{post.primary_author?.profile_image && (
|
||||
<div className="relative h-6 w-6 rounded-full overflow-hidden mr-2">
|
||||
<div className="relative mr-2 h-6 w-6 overflow-hidden rounded-full">
|
||||
{post.primary_author.twitter ? (
|
||||
<button
|
||||
className="block cursor-pointer transition-opacity hover:opacity-90"
|
||||
@@ -55,14 +55,20 @@ export function BlogPostCard({ post }: BlogPostCardProps) {
|
||||
type="button"
|
||||
>
|
||||
<img
|
||||
src={post.primary_author.profile_image}
|
||||
src={
|
||||
post.primary_author
|
||||
.profile_image
|
||||
}
|
||||
alt={post.primary_author.name}
|
||||
className="object-cover"
|
||||
/>
|
||||
</button>
|
||||
) : (
|
||||
<img
|
||||
src={post.primary_author.profile_image}
|
||||
src={
|
||||
post.primary_author
|
||||
.profile_image
|
||||
}
|
||||
alt={post.primary_author.name}
|
||||
className="object-cover"
|
||||
/>
|
||||
@@ -71,18 +77,22 @@ export function BlogPostCard({ post }: BlogPostCardProps) {
|
||||
)}
|
||||
{post.primary_author?.twitter ? (
|
||||
<button
|
||||
className="hover:text-primary transition-colors"
|
||||
className="transition-colors hover:text-primary"
|
||||
onClick={handleTwitterClick}
|
||||
type="button"
|
||||
>
|
||||
{post.primary_author.name || "Unknown Author"}
|
||||
{post.primary_author.name ||
|
||||
'Unknown Author'}
|
||||
</button>
|
||||
) : (
|
||||
<span>{post.primary_author?.name || "Unknown Author"}</span>
|
||||
<span>
|
||||
{post.primary_author?.name ||
|
||||
'Unknown Author'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="mx-2">in</span>
|
||||
<span>{post.primary_tag?.name || "General"}</span>
|
||||
<span>{post.primary_tag?.name || 'General'}</span>
|
||||
<span className="mx-2">•</span>
|
||||
<span>{post.reading_time} min read</span>
|
||||
<span className="mx-2">•</span>
|
||||
@@ -91,5 +101,5 @@ export function BlogPostCard({ post }: BlogPostCardProps) {
|
||||
</div>
|
||||
</article>
|
||||
</Link>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import {
|
||||
Select,
|
||||
@@ -6,27 +6,27 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useDebounce } from "@/lib/hooks/use-debounce";
|
||||
import { Search } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useTransition } from "react";
|
||||
} from '@/components/ui/select'
|
||||
import { useDebounce } from '@/lib/hooks/use-debounce'
|
||||
import { Search } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useCallback, useTransition } from 'react'
|
||||
|
||||
interface Tag {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
}
|
||||
|
||||
interface SearchAndFilterProps {
|
||||
tags: Tag[];
|
||||
initialSearch: string;
|
||||
initialTag: string;
|
||||
searchPlaceholder: string;
|
||||
allTagsText: string;
|
||||
tags: Tag[]
|
||||
initialSearch: string
|
||||
initialTag: string
|
||||
searchPlaceholder: string
|
||||
allTagsText: string
|
||||
}
|
||||
|
||||
const ALL_TAGS_VALUE = "all";
|
||||
const ALL_TAGS_VALUE = 'all'
|
||||
|
||||
export function SearchAndFilter({
|
||||
tags,
|
||||
@@ -35,44 +35,44 @@ export function SearchAndFilter({
|
||||
searchPlaceholder,
|
||||
allTagsText,
|
||||
}: SearchAndFilterProps) {
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const router = useRouter()
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
const handleTagChange = (value: string) => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const searchParams = new URLSearchParams(window.location.search)
|
||||
if (value && value !== ALL_TAGS_VALUE) {
|
||||
searchParams.set("tag", value);
|
||||
searchParams.set('tag', value)
|
||||
} else {
|
||||
searchParams.delete("tag");
|
||||
searchParams.delete('tag')
|
||||
}
|
||||
startTransition(() => {
|
||||
router.push(`?${searchParams.toString()}`);
|
||||
});
|
||||
};
|
||||
router.push(`?${searchParams.toString()}`)
|
||||
})
|
||||
}
|
||||
|
||||
const debouncedCallback = useDebounce((value: string) => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const searchParams = new URLSearchParams(window.location.search)
|
||||
if (value) {
|
||||
searchParams.set("search", value);
|
||||
searchParams.set('search', value)
|
||||
} else {
|
||||
searchParams.delete("search");
|
||||
searchParams.delete('search')
|
||||
}
|
||||
startTransition(() => {
|
||||
router.push(`?${searchParams.toString()}`);
|
||||
});
|
||||
}, 300);
|
||||
router.push(`?${searchParams.toString()}`)
|
||||
})
|
||||
}, 300)
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
debouncedCallback(e.target.value);
|
||||
debouncedCallback(e.target.value)
|
||||
},
|
||||
[debouncedCallback],
|
||||
);
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row gap-4 mb-8">
|
||||
<div className="mb-8 flex flex-col gap-4 md:flex-row">
|
||||
<div className="relative flex-1">
|
||||
<div className="absolute inset-y-0 left-3 flex items-center pointer-events-none">
|
||||
<div className="pointer-events-none absolute inset-y-0 left-3 flex items-center">
|
||||
<Search className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
@@ -80,7 +80,7 @@ export function SearchAndFilter({
|
||||
defaultValue={initialSearch}
|
||||
onChange={handleSearch}
|
||||
placeholder={searchPlaceholder}
|
||||
className="w-full pl-10 pr-4 py-2 border border-border rounded-md bg-background ring-offset-background placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className="w-full rounded-md border border-border bg-background py-2 pl-10 pr-4 ring-offset-background placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-64">
|
||||
@@ -92,7 +92,9 @@ export function SearchAndFilter({
|
||||
<SelectValue placeholder={allTagsText} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL_TAGS_VALUE}>{allTagsText}</SelectItem>
|
||||
<SelectItem value={ALL_TAGS_VALUE}>
|
||||
{allTagsText}
|
||||
</SelectItem>
|
||||
{tags.map((tag) => (
|
||||
<SelectItem key={tag.id} value={tag.slug}>
|
||||
{tag.name}
|
||||
@@ -102,5 +104,5 @@ export function SearchAndFilter({
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,55 +1,58 @@
|
||||
import { getPosts, getTags } from "@/lib/ghost";
|
||||
import type { Post } from "@/lib/ghost";
|
||||
import { RssIcon } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { BlogPostCard } from "./components/BlogPostCard";
|
||||
import { SearchAndFilter } from "./components/SearchAndFilter";
|
||||
import { getPosts, getTags } from '@/lib/ghost'
|
||||
import type { Post } from '@/lib/ghost'
|
||||
import { RssIcon } from 'lucide-react'
|
||||
import type { Metadata } from 'next'
|
||||
import Link from 'next/link'
|
||||
import { BlogPostCard } from './components/BlogPostCard'
|
||||
import { SearchAndFilter } from './components/SearchAndFilter'
|
||||
|
||||
interface Tag {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Blog",
|
||||
description: "Latest news, updates, and articles from Dokploy",
|
||||
};
|
||||
title: 'Blog',
|
||||
description: 'Latest news, updates, and articles from Dokploy',
|
||||
}
|
||||
|
||||
export default async function BlogPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
searchParams: { [key: string]: string | string[] | undefined }
|
||||
}) {
|
||||
const searchParams2 = await searchParams;
|
||||
const posts = await getPosts();
|
||||
const tags = (await getTags()) as Tag[];
|
||||
const searchParams2 = await searchParams
|
||||
const posts = await getPosts()
|
||||
const tags = (await getTags()) as Tag[]
|
||||
const search =
|
||||
typeof searchParams2.search === "string" ? searchParams2.search : "";
|
||||
typeof searchParams2.search === 'string' ? searchParams2.search : ''
|
||||
const selectedTag =
|
||||
typeof searchParams2.tag === "string" ? searchParams2.tag : "";
|
||||
typeof searchParams2.tag === 'string' ? searchParams2.tag : ''
|
||||
|
||||
const filteredPosts = posts.filter((post) => {
|
||||
const matchesSearch =
|
||||
search === "" ||
|
||||
search === '' ||
|
||||
post.title.toLowerCase().includes(search.toLowerCase()) ||
|
||||
post.excerpt.toLowerCase().includes(search.toLowerCase());
|
||||
post.excerpt.toLowerCase().includes(search.toLowerCase())
|
||||
|
||||
const matchesTag =
|
||||
selectedTag === "" || post.tags?.some((tag) => tag.slug === selectedTag);
|
||||
selectedTag === '' ||
|
||||
post.tags?.some((tag) => tag.slug === selectedTag)
|
||||
|
||||
return matchesSearch && matchesTag;
|
||||
});
|
||||
return matchesSearch && matchesTag
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-12 max-w-5xl">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="container mx-auto max-w-5xl px-4 py-12">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground uppercase tracking-wider mb-2">
|
||||
<p className="mb-2 text-sm uppercase tracking-wider text-muted-foreground">
|
||||
BLOG
|
||||
</p>
|
||||
<h1 className="text-4xl font-bold">Dokploy Latest News & Updates</h1>
|
||||
<h1 className="text-4xl font-bold">
|
||||
Dokploy Latest News & Updates
|
||||
</h1>
|
||||
</div>
|
||||
<Link
|
||||
href="/rss.xml"
|
||||
@@ -68,9 +71,11 @@ export default async function BlogPage({
|
||||
/>
|
||||
|
||||
{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">
|
||||
{search || selectedTag ? "No posts found matching your criteria" : "No posts available"}
|
||||
{search || selectedTag
|
||||
? 'No posts found matching your criteria'
|
||||
: 'No posts available'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -81,5 +86,5 @@ export default async function BlogPage({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,59 +1,61 @@
|
||||
import { getPostsByTag, getTags } from "@/lib/ghost";
|
||||
import type { Post } from "@/lib/ghost";
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getPostsByTag, getTags } from '@/lib/ghost'
|
||||
import type { Post } from '@/lib/ghost'
|
||||
import type { Metadata } from 'next'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
type Props = {
|
||||
params: { tag: string };
|
||||
};
|
||||
params: { tag: string }
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { tag } = await params;
|
||||
const posts = await getPostsByTag(tag);
|
||||
const { tag } = await params
|
||||
const posts = await getPostsByTag(tag)
|
||||
|
||||
if (!posts || posts.length === 0) {
|
||||
return {
|
||||
title: "Tag Not Found",
|
||||
description: "The requested tag could not be found",
|
||||
};
|
||||
title: 'Tag Not Found',
|
||||
description: 'The requested tag could not be found',
|
||||
}
|
||||
}
|
||||
|
||||
const tagName =
|
||||
posts[0].tags?.find((t: { slug: string }) => t.slug === tag)?.name || tag;
|
||||
posts[0].tags?.find((t: { slug: string }) => t.slug === tag)?.name ||
|
||||
tag
|
||||
|
||||
return {
|
||||
title: `${tagName} Posts`,
|
||||
description: `Browse all posts tagged with ${tagName}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const tags = await getTags();
|
||||
return tags.map((tag: { slug: string }) => ({ tag: tag.slug }));
|
||||
const tags = await getTags()
|
||||
return tags.map((tag: { slug: string }) => ({ tag: tag.slug }))
|
||||
}
|
||||
|
||||
export default async function TagPage({ params }: Props) {
|
||||
const { tag } = await params;
|
||||
const posts = await getPostsByTag(tag);
|
||||
const { tag } = await params
|
||||
const posts = await getPostsByTag(tag)
|
||||
|
||||
if (!posts || posts.length === 0) {
|
||||
notFound();
|
||||
notFound()
|
||||
}
|
||||
|
||||
const tagName =
|
||||
posts[0].tags?.find((t: { slug: string }) => t.slug === tag)?.name || tag;
|
||||
posts[0].tags?.find((t: { slug: string }) => t.slug === tag)?.name ||
|
||||
tag
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<Link
|
||||
href="/blog"
|
||||
className="inline-flex items-center mb-8 text-primary-600 hover:text-primary-800 transition-colors"
|
||||
className="text-primary-600 hover:text-primary-800 mb-8 inline-flex items-center transition-colors"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5 mr-2"
|
||||
className="mr-2 h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
@@ -67,8 +69,8 @@ export default async function TagPage({ params }: Props) {
|
||||
</Link>
|
||||
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold mb-2">
|
||||
Posts tagged with{" "}
|
||||
<h1 className="mb-2 text-3xl font-bold">
|
||||
Posts tagged with{' '}
|
||||
<span className="text-primary-600">"{tagName}"</span>
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
@@ -76,25 +78,25 @@ export default async function TagPage({ params }: Props) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{posts.map((post: Post) => (
|
||||
<BlogPostCard key={post.id} post={post} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function BlogPostCard({ post }: { post: Post }) {
|
||||
const formattedDate = new Date(post.published_at).toLocaleDateString("en", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
const formattedDate = new Date(post.published_at).toLocaleDateString('en', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
|
||||
return (
|
||||
<Link href={`/blog/${post.slug}`} className="group">
|
||||
<div className="dark:bg-gray-800 rounded-lg overflow-hidden shadow-lg transition-all duration-300 hover:shadow-xl">
|
||||
<div className="overflow-hidden rounded-lg shadow-lg transition-all duration-300 hover:shadow-xl dark:bg-gray-800">
|
||||
{post.feature_image && (
|
||||
<div className="relative h-48 w-full">
|
||||
<Image
|
||||
@@ -106,18 +108,18 @@ function BlogPostCard({ post }: { post: Post }) {
|
||||
</div>
|
||||
)}
|
||||
<div className="p-6">
|
||||
<h2 className="text-xl font-semibold mb-2 group-hover:text-primary-500 transition-colors">
|
||||
<h2 className="group-hover:text-primary-500 mb-2 text-xl font-semibold transition-colors">
|
||||
{post.title}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
||||
<p className="mb-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{formattedDate} • {post.reading_time} min read
|
||||
</p>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
<p className="mb-4 text-gray-700 dark:text-gray-300">
|
||||
{post.custom_excerpt || post.excerpt}
|
||||
</p>
|
||||
<div className="flex items-center">
|
||||
{post.primary_author?.profile_image && (
|
||||
<div className="relative h-10 w-10 rounded-full overflow-hidden mr-3">
|
||||
<div className="relative mr-3 h-10 w-10 overflow-hidden rounded-full">
|
||||
<Image
|
||||
src={post.primary_author.profile_image}
|
||||
alt={post.primary_author.name}
|
||||
@@ -128,12 +130,12 @@ function BlogPostCard({ post }: { post: Post }) {
|
||||
)}
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{post.primary_author?.name || "Unknown Author"}
|
||||
{post.primary_author?.name || 'Unknown Author'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,13 +1,12 @@
|
||||
import type { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from 'next'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Contact Us",
|
||||
title: 'Contact Us',
|
||||
description:
|
||||
"Get in touch with our team. We're here to help with any questions about Dokploy.",
|
||||
};
|
||||
|
||||
export default function ContactLayout({ children }: { children: ReactNode }) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export default function ContactLayout({ children }: { children: ReactNode }) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
@@ -1,126 +1,126 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { useState } from "react";
|
||||
import { Container } from "@/components/Container";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useState } from 'react'
|
||||
import { Container } from '@/components/Container'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { trackGAEvent } from "@/components/analitycs";
|
||||
import AnimatedGridPattern from "@/components/ui/animated-grid-pattern";
|
||||
import { cn } from "@/lib/utils";
|
||||
} from '@/components/ui/select'
|
||||
import { trackGAEvent } from '@/components/analitycs'
|
||||
import AnimatedGridPattern from '@/components/ui/animated-grid-pattern'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface ContactFormData {
|
||||
inquiryType: "" | "support" | "sales" | "other";
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
company: string;
|
||||
message: string;
|
||||
inquiryType: '' | 'support' | 'sales' | 'other'
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
company: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export default function ContactPage() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [isSubmitted, setIsSubmitted] = useState(false)
|
||||
const [formData, setFormData] = useState<ContactFormData>({
|
||||
inquiryType: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
company: "",
|
||||
message: "",
|
||||
});
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
inquiryType: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
company: '',
|
||||
message: '',
|
||||
})
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
const newErrors: Record<string, string> = {}
|
||||
|
||||
if (!formData.inquiryType) {
|
||||
newErrors.inquiryType = "Please select what we can help you with";
|
||||
newErrors.inquiryType = 'Please select what we can help you with'
|
||||
}
|
||||
if (!formData.firstName.trim()) {
|
||||
newErrors.firstName = "First name is required";
|
||||
newErrors.firstName = 'First name is required'
|
||||
}
|
||||
if (!formData.lastName.trim()) {
|
||||
newErrors.lastName = "Last name is required";
|
||||
newErrors.lastName = 'Last name is required'
|
||||
}
|
||||
if (!formData.email.trim()) {
|
||||
newErrors.email = "Email is required";
|
||||
newErrors.email = 'Email is required'
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||
newErrors.email = "Please enter a valid email address";
|
||||
newErrors.email = 'Please enter a valid email address'
|
||||
}
|
||||
if (!formData.company.trim()) {
|
||||
newErrors.company = "Company name is required";
|
||||
newErrors.company = 'Company name is required'
|
||||
}
|
||||
if (!formData.message.trim()) {
|
||||
newErrors.message = "Message is required";
|
||||
newErrors.message = 'Message is required'
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/contact", {
|
||||
method: "POST",
|
||||
const response = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
trackGAEvent({
|
||||
action: "Contact Form Submitted",
|
||||
category: "Contact",
|
||||
action: 'Contact Form Submitted',
|
||||
category: 'Contact',
|
||||
label: formData.inquiryType,
|
||||
});
|
||||
})
|
||||
|
||||
setFormData({
|
||||
inquiryType: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
company: "",
|
||||
message: "",
|
||||
});
|
||||
setErrors({});
|
||||
setIsSubmitted(true);
|
||||
inquiryType: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
company: '',
|
||||
message: '',
|
||||
})
|
||||
setErrors({})
|
||||
setIsSubmitted(true)
|
||||
} else {
|
||||
throw new Error("Failed to submit form");
|
||||
throw new Error('Failed to submit form')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error submitting form:", error);
|
||||
alert("There was an error sending your message. Please try again.");
|
||||
console.error('Error submitting form:', error)
|
||||
alert('There was an error sending your message. Please try again.')
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (field: keyof ContactFormData, value: any) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
setFormData((prev) => ({ ...prev, [field]: value }))
|
||||
if (errors[field]) {
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors[field];
|
||||
return newErrors;
|
||||
});
|
||||
const newErrors = { ...prev }
|
||||
delete newErrors[field]
|
||||
return newErrors
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (isSubmitted) {
|
||||
return (
|
||||
@@ -131,22 +131,25 @@ export default function ContactPage() {
|
||||
Thank you for contacting us!
|
||||
</h1>
|
||||
<p className="mt-6 text-lg leading-8 text-muted-foreground">
|
||||
We've received your message and will get back to you as soon as
|
||||
possible.
|
||||
We've received your message and will get back to you
|
||||
as soon as possible.
|
||||
</p>
|
||||
<div className="mt-10">
|
||||
<Button onClick={() => setIsSubmitted(false)} variant="outline">
|
||||
<Button
|
||||
onClick={() => setIsSubmitted(false)}
|
||||
variant="outline"
|
||||
>
|
||||
Send Another Message
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-background py-24 sm:py-32 relative">
|
||||
<div className="relative bg-background py-24 sm:py-32">
|
||||
<AnimatedGridPattern
|
||||
numSquares={30}
|
||||
maxOpacity={0.1}
|
||||
@@ -155,19 +158,19 @@ export default function ContactPage() {
|
||||
duration={3}
|
||||
repeatDelay={1}
|
||||
className={cn(
|
||||
"[mask-image:radial-gradient(800px_circle_at_center,white,transparent)]",
|
||||
"absolute inset-x-0 inset-y-[-30%] h-[200%] skew-y-12",
|
||||
'[mask-image:radial-gradient(800px_circle_at_center,white,transparent)]',
|
||||
'absolute inset-x-0 inset-y-[-30%] h-[200%] skew-y-12',
|
||||
)}
|
||||
/>
|
||||
<Container>
|
||||
<div className="mx-auto max-w-3xl border border-border rounded-lg p-8 bg-black z-10 relative">
|
||||
<div className="relative z-10 mx-auto max-w-3xl rounded-lg border border-border bg-black p-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
||||
Contact Us
|
||||
</h1>
|
||||
<p className="mt-6 text-lg leading-8 text-muted-foreground">
|
||||
Get in touch with our team. We're here to help with any questions
|
||||
about Dokploy.
|
||||
Get in touch with our team. We're here to help with
|
||||
any questions about Dokploy.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -177,15 +180,15 @@ export default function ContactPage() {
|
||||
htmlFor="inquiryType"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
What can we help you with today?{" "}
|
||||
What can we help you with today?{' '}
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Select
|
||||
value={formData.inquiryType}
|
||||
onValueChange={(value) =>
|
||||
handleInputChange(
|
||||
"inquiryType",
|
||||
value as "support" | "sales" | "other",
|
||||
'inquiryType',
|
||||
value as 'support' | 'sales' | 'other',
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -193,13 +196,17 @@ export default function ContactPage() {
|
||||
<SelectValue placeholder="Select an option" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="support">Support</SelectItem>
|
||||
<SelectItem value="support">
|
||||
Support
|
||||
</SelectItem>
|
||||
<SelectItem value="sales">Sales</SelectItem>
|
||||
<SelectItem value="other">Other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.inquiryType && (
|
||||
<p className="text-sm text-red-600">{errors.inquiryType}</p>
|
||||
<p className="text-sm text-red-600">
|
||||
{errors.inquiryType}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -209,19 +216,25 @@ export default function ContactPage() {
|
||||
htmlFor="firstName"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
First Name <span className="text-red-500">*</span>
|
||||
First Name{' '}
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="firstName"
|
||||
type="text"
|
||||
value={formData.firstName}
|
||||
onChange={(e) =>
|
||||
handleInputChange("firstName", e.target.value)
|
||||
handleInputChange(
|
||||
'firstName',
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
placeholder="Your first name"
|
||||
/>
|
||||
{errors.firstName && (
|
||||
<p className="text-sm text-red-600">{errors.firstName}</p>
|
||||
<p className="text-sm text-red-600">
|
||||
{errors.firstName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -230,19 +243,25 @@ export default function ContactPage() {
|
||||
htmlFor="lastName"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
Last Name <span className="text-red-500">*</span>
|
||||
Last Name{' '}
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="lastName"
|
||||
type="text"
|
||||
value={formData.lastName}
|
||||
onChange={(e) =>
|
||||
handleInputChange("lastName", e.target.value)
|
||||
handleInputChange(
|
||||
'lastName',
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
placeholder="Your last name"
|
||||
/>
|
||||
{errors.lastName && (
|
||||
<p className="text-sm text-red-600">{errors.lastName}</p>
|
||||
<p className="text-sm text-red-600">
|
||||
{errors.lastName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -258,11 +277,15 @@ export default function ContactPage() {
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange("email", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleInputChange('email', e.target.value)
|
||||
}
|
||||
placeholder="your.email@company.com"
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-sm text-red-600">{errors.email}</p>
|
||||
<p className="text-sm text-red-600">
|
||||
{errors.email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -271,17 +294,22 @@ export default function ContactPage() {
|
||||
htmlFor="company"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
Company Name <span className="text-red-500">*</span>
|
||||
Company Name{' '}
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="company"
|
||||
type="text"
|
||||
value={formData.company}
|
||||
onChange={(e) => handleInputChange("company", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleInputChange('company', e.target.value)
|
||||
}
|
||||
placeholder="Your company name"
|
||||
/>
|
||||
{errors.company && (
|
||||
<p className="text-sm text-red-600">{errors.company}</p>
|
||||
<p className="text-sm text-red-600">
|
||||
{errors.company}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -290,18 +318,23 @@ export default function ContactPage() {
|
||||
htmlFor="message"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
How can we help? <span className="text-red-500">*</span>
|
||||
How can we help?{' '}
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
value={formData.message}
|
||||
onChange={(e) => handleInputChange("message", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleInputChange('message', e.target.value)
|
||||
}
|
||||
placeholder="Tell us more about your inquiry..."
|
||||
rows={6}
|
||||
className="flex w-full rounded-md bg-input border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-none"
|
||||
className="flex w-full resize-none rounded-md border border-input bg-background bg-input px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
{errors.message && (
|
||||
<p className="text-sm text-red-600">{errors.message}</p>
|
||||
<p className="text-sm text-red-600">
|
||||
{errors.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -311,12 +344,12 @@ export default function ContactPage() {
|
||||
disabled={isSubmitting}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
{isSubmitting ? "Sending..." : "Send Message"}
|
||||
{isSubmitting ? 'Sending...' : 'Send Message'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
import clsx from "clsx";
|
||||
import type { Metadata } from "next";
|
||||
import { Inter, Lexend } from "next/font/google";
|
||||
import type { ReactNode } from "react";
|
||||
import { GoogleAnalytics } from "@next/third-parties/google";
|
||||
import "@/styles/tailwind.css";
|
||||
import "react-photo-view/dist/react-photo-view.css";
|
||||
import { Header } from "@/components/Header";
|
||||
import { Footer } from "@/components/Footer";
|
||||
import clsx from 'clsx'
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter, Lexend } from 'next/font/google'
|
||||
import type { ReactNode } from 'react'
|
||||
import { GoogleAnalytics } from '@next/third-parties/google'
|
||||
import '@/styles/tailwind.css'
|
||||
import 'react-photo-view/dist/react-photo-view.css'
|
||||
import { Header } from '@/components/Header'
|
||||
import { Footer } from '@/components/Footer'
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://dokploy.com"),
|
||||
metadataBase: new URL('https://dokploy.com'),
|
||||
title: {
|
||||
default: "Dokploy - Deploy your applications with ease",
|
||||
template: "%s | Dokploy",
|
||||
default: 'Dokploy - Deploy your applications with ease',
|
||||
template: '%s | Dokploy',
|
||||
},
|
||||
description: "Deploy your applications with ease using Dokploy",
|
||||
description: 'Deploy your applications with ease using Dokploy',
|
||||
icons: {
|
||||
icon: "icon.svg",
|
||||
apple: "apple-touch-icon.png",
|
||||
icon: 'icon.svg',
|
||||
apple: 'apple-touch-icon.png',
|
||||
},
|
||||
openGraph: {
|
||||
title: "Dokploy - Deploy your applications with ease",
|
||||
description: "Deploy your applications with ease using Dokploy",
|
||||
images: "/og.png",
|
||||
type: "website",
|
||||
title: 'Dokploy - Deploy your applications with ease',
|
||||
description: 'Deploy your applications with ease using Dokploy',
|
||||
images: '/og.png',
|
||||
type: 'website',
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Dokploy - Deploy your applications with ease",
|
||||
description: "Deploy your applications with ease using Dokploy",
|
||||
images: ["/og.png"],
|
||||
card: 'summary_large_image',
|
||||
title: 'Dokploy - Deploy your applications with ease',
|
||||
description: 'Deploy your applications with ease using Dokploy',
|
||||
images: ['/og.png'],
|
||||
},
|
||||
};
|
||||
}
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
variable: "--font-inter",
|
||||
});
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
variable: '--font-inter',
|
||||
})
|
||||
|
||||
const lexend = Lexend({
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
variable: "--font-lexend",
|
||||
});
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
variable: '--font-lexend',
|
||||
})
|
||||
// Since we have a `not-found.tsx` page on the root, a layout file
|
||||
// is required, even if it's just passing children through.
|
||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
@@ -54,7 +54,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
<html
|
||||
lang="en"
|
||||
className={clsx(
|
||||
"h-full scroll-smooth antialiased",
|
||||
'h-full scroll-smooth antialiased',
|
||||
inter.variable,
|
||||
lexend.variable,
|
||||
)}
|
||||
@@ -77,5 +77,5 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import NextError from "next/error";
|
||||
import NextError from 'next/error'
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
@@ -9,5 +9,5 @@ export default function NotFound() {
|
||||
<NextError statusCode={404} />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { CallToAction } from "@/components/CallToAction";
|
||||
import { Faqs } from "@/components/Faqs";
|
||||
import { Hero } from "@/components/Hero";
|
||||
import { Testimonials } from "@/components/Testimonials";
|
||||
import { FirstFeaturesSection } from "@/components/first-features";
|
||||
import { Pricing } from "@/components/pricing";
|
||||
import { SecondaryFeaturesSections } from "@/components/secondary-features";
|
||||
import { Sponsors } from "@/components/sponsors";
|
||||
import { StatsSection } from "@/components/stats";
|
||||
import type { Metadata } from "next";
|
||||
import { CallToAction } from '@/components/CallToAction'
|
||||
import { Faqs } from '@/components/Faqs'
|
||||
import { Hero } from '@/components/Hero'
|
||||
import { Testimonials } from '@/components/Testimonials'
|
||||
import { FirstFeaturesSection } from '@/components/first-features'
|
||||
import { Pricing } from '@/components/pricing'
|
||||
import { SecondaryFeaturesSections } from '@/components/secondary-features'
|
||||
import { Sponsors } from '@/components/sponsors'
|
||||
import { StatsSection } from '@/components/stats'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: "Dokploy - Deploy your applications with ease",
|
||||
absolute: 'Dokploy - Deploy your applications with ease',
|
||||
},
|
||||
description: "Open-source self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases",
|
||||
};
|
||||
description:
|
||||
'Open-source self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases',
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
@@ -25,7 +26,7 @@ export default function Home() {
|
||||
<SecondaryFeaturesSections />
|
||||
<StatsSection />
|
||||
<Testimonials />
|
||||
<div className="w-full relative">
|
||||
<div className="relative w-full">
|
||||
<Pricing />
|
||||
</div>
|
||||
<Faqs />
|
||||
@@ -33,5 +34,5 @@ export default function Home() {
|
||||
<CallToAction />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,37 +1,43 @@
|
||||
import type { Metadata } from "next";
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Privacy Policy",
|
||||
title: 'Privacy Policy',
|
||||
description:
|
||||
"Learn about how Dokploy collects, uses, and safeguards your personal information when you use our website and services.",
|
||||
};
|
||||
'Learn about how Dokploy collects, uses, and safeguards your personal information when you use our website and services.',
|
||||
}
|
||||
|
||||
export default function PrivacyPage() {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full max-w-4xl mx-auto py-12 px-4">
|
||||
<h1 className="text-3xl font-bold text-center mb-6">Privacy</h1>
|
||||
<div className="mx-auto flex w-full max-w-4xl flex-col gap-4 px-4 py-12">
|
||||
<h1 className="mb-6 text-center text-3xl font-bold">Privacy</h1>
|
||||
|
||||
<section className="flex flex-col gap-2">
|
||||
<p>
|
||||
At Dokploy, we are committed to protecting your privacy. This Privacy
|
||||
Policy explains how we collect, use, and safeguard your personal
|
||||
information when you use our website and services.
|
||||
At Dokploy, we are committed to protecting your privacy.
|
||||
This Privacy Policy explains how we collect, use, and
|
||||
safeguard your personal information when you use our website
|
||||
and services.
|
||||
</p>
|
||||
<p>
|
||||
By using Dokploy, you agree to the collection and use of information
|
||||
in accordance with this Privacy Policy. If you do not agree with these
|
||||
practices, please do not use our services.
|
||||
By using Dokploy, you agree to the collection and use of
|
||||
information in accordance with this Privacy Policy. If you
|
||||
do not agree with these practices, please do not use our
|
||||
services.
|
||||
</p>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
<h2 className="mb-4 text-2xl font-semibold">
|
||||
1. Information We Collect
|
||||
</h2>
|
||||
<p className="">
|
||||
We only collect limited, non-personal data through Umami Analytics, a
|
||||
privacy-focused analytics tool. No personal identifying information
|
||||
(PII) is collected. The data we collect includes:
|
||||
We only collect limited, non-personal data through Umami
|
||||
Analytics, a privacy-focused analytics tool. No personal
|
||||
identifying information (PII) is collected. The data we
|
||||
collect includes:
|
||||
</p>
|
||||
<ul className="list-disc list-inside mb-4">
|
||||
<li>Website usage statistics (e.g., page views, session duration)</li>
|
||||
<ul className="mb-4 list-inside list-disc">
|
||||
<li>
|
||||
Website usage statistics (e.g., page views, session
|
||||
duration)
|
||||
</li>
|
||||
<li>Anonymized IP addresses</li>
|
||||
<li>Referring websites</li>
|
||||
<li>Browser and device type</li>
|
||||
@@ -39,73 +45,80 @@ export default function PrivacyPage() {
|
||||
</section>
|
||||
|
||||
<section className="">
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
<h2 className="mb-4 text-2xl font-semibold">
|
||||
2. How We Use the Information
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
The information we collect is used solely for improving the
|
||||
functionality and user experience of our platform. Specifically, we
|
||||
use it to:
|
||||
functionality and user experience of our platform.
|
||||
Specifically, we use it to:
|
||||
</p>
|
||||
<ul className="list-disc list-inside mb-4">
|
||||
<ul className="mb-4 list-inside list-disc">
|
||||
<li>Monitor traffic and website performance</li>
|
||||
<li>Optimize the user experience</li>
|
||||
<li>Understand how users interact with our platform</li>
|
||||
</ul>
|
||||
<p>
|
||||
Additionally, we use a single cookie to manage user sessions, which is
|
||||
necessary for the proper functioning of the platform.
|
||||
Additionally, we use a single cookie to manage user
|
||||
sessions, which is necessary for the proper functioning of
|
||||
the platform.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-2">
|
||||
<h2 className="text-2xl font-semibold mb-4">3. Data Security</h2>
|
||||
<h2 className="mb-4 text-2xl font-semibold">
|
||||
3. Data Security
|
||||
</h2>
|
||||
<p className="">
|
||||
We take reasonable precautions to protect your data. Since we do not
|
||||
collect personal information, the risk of data misuse is minimized.
|
||||
Umami Analytics is privacy-friendly and does not rely on cookies or
|
||||
store PII.
|
||||
We take reasonable precautions to protect your data. Since
|
||||
we do not collect personal information, the risk of data
|
||||
misuse is minimized. Umami Analytics is privacy-friendly and
|
||||
does not rely on cookies or store PII.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="">
|
||||
<h2 className="text-2xl font-semibold mb-4">4. Third-Party Services</h2>
|
||||
<h2 className="mb-4 text-2xl font-semibold">
|
||||
4. Third-Party Services
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
We do not share your data with any third-party services other than
|
||||
Umami Analytics. We do not sell, trade, or transfer your data to
|
||||
outside parties.
|
||||
We do not share your data with any third-party services
|
||||
other than Umami Analytics. We do not sell, trade, or
|
||||
transfer your data to outside parties.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="">
|
||||
<h2 className="text-2xl font-semibold mb-4">5. Cookies</h2>
|
||||
<h2 className="mb-4 text-2xl font-semibold">5. Cookies</h2>
|
||||
<p className="mb-4">
|
||||
Dokploy does not use cookies to track user activity. Umami Analytics
|
||||
is cookie-free and does not require any tracking cookies for its
|
||||
functionality.
|
||||
Dokploy does not use cookies to track user activity. Umami
|
||||
Analytics is cookie-free and does not require any tracking
|
||||
cookies for its functionality.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-2">
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
<h2 className="mb-4 text-2xl font-semibold">
|
||||
6. Changes to This Privacy Policy
|
||||
</h2>
|
||||
<p className="">
|
||||
We may update this Privacy Policy from time to time. Any changes will
|
||||
be posted on this page, and it is your responsibility to review this
|
||||
policy periodically.
|
||||
We may update this Privacy Policy from time to time. Any
|
||||
changes will be posted on this page, and it is your
|
||||
responsibility to review this policy periodically.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="">
|
||||
<h2 className="text-2xl font-semibold mb-4">12. Contact Information</h2>
|
||||
<h2 className="mb-4 text-2xl font-semibold">
|
||||
12. Contact Information
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
If you have any questions or concerns regarding these Privacy Policy,
|
||||
please contact us at:
|
||||
If you have any questions or concerns regarding these
|
||||
Privacy Policy, please contact us at:
|
||||
</p>
|
||||
<p className="mb-4">
|
||||
Email:{" "}
|
||||
Email:{' '}
|
||||
<a
|
||||
href="mailto:support@dokploy.com"
|
||||
className="text-blue-500 hover:underline"
|
||||
@@ -115,6 +128,5 @@ export default function PrivacyPage() {
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
import type { MetadataRoute } from 'next'
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
userAgent: '*',
|
||||
allow: '/',
|
||||
},
|
||||
sitemap: "https://dokploy.com/sitemap.xml",
|
||||
};
|
||||
sitemap: 'https://dokploy.com/sitemap.xml',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import { getPosts } from "@/lib/ghost";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getPosts } from '@/lib/ghost'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
function escapeXml(unsafe: string): string {
|
||||
return unsafe.replace(/[<>&'"]/g, (c) => {
|
||||
switch (c) {
|
||||
case "<":
|
||||
return "<";
|
||||
case ">":
|
||||
return ">";
|
||||
case "&":
|
||||
return "&";
|
||||
case '<':
|
||||
return '<'
|
||||
case '>':
|
||||
return '>'
|
||||
case '&':
|
||||
return '&'
|
||||
case "'":
|
||||
return "'";
|
||||
return '''
|
||||
case '"':
|
||||
return """;
|
||||
return '"'
|
||||
default:
|
||||
return c;
|
||||
return c
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const posts = await getPosts();
|
||||
const posts = await getPosts()
|
||||
|
||||
const rss = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<channel>
|
||||
<title>Dokploy Blog</title>
|
||||
<link>https://dokploy.com/blog</link>
|
||||
<description>${escapeXml("Dokploy Latest News & Updates")}</description>
|
||||
<description>${escapeXml('Dokploy Latest News & Updates')}</description>
|
||||
<language>en</language>
|
||||
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
|
||||
${posts
|
||||
@@ -44,23 +44,23 @@ export async function GET() {
|
||||
${
|
||||
post.feature_image
|
||||
? `<enclosure url="${escapeXml(post.feature_image)}" type="image/jpeg" />`
|
||||
: ""
|
||||
: ''
|
||||
}
|
||||
${
|
||||
post.primary_author
|
||||
? `<dc:creator><![CDATA[${post.primary_author.name}]]></dc:creator>`
|
||||
: ""
|
||||
: ''
|
||||
}
|
||||
</item>`,
|
||||
)
|
||||
.join("\n")}
|
||||
.join('\n')}
|
||||
</channel>
|
||||
</rss>`;
|
||||
</rss>`
|
||||
|
||||
return new NextResponse(rss, {
|
||||
headers: {
|
||||
"Content-Type": "application/xml; charset=utf-8",
|
||||
"Cache-Control": "s-maxage=3600, stale-while-revalidate",
|
||||
'Content-Type': 'application/xml; charset=utf-8',
|
||||
'Cache-Control': 's-maxage=3600, stale-while-revalidate',
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { getPosts } from "@/lib/ghost";
|
||||
import type { MetadataRoute } from "next";
|
||||
import { getPosts } from '@/lib/ghost'
|
||||
import type { MetadataRoute } from 'next'
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const posts = await getPosts();
|
||||
const posts = await getPosts()
|
||||
return [
|
||||
{
|
||||
url: "https://dokploy.com",
|
||||
url: 'https://dokploy.com',
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
changeFrequency: 'monthly',
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: "https://dokploy.com/blog",
|
||||
url: 'https://dokploy.com/blog',
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.8,
|
||||
},
|
||||
...posts.map((post) => ({
|
||||
url: `https://dokploy.com/blog/${post.slug}`,
|
||||
lastModified: new Date(post.published_at),
|
||||
changeFrequency: "monthly" as const,
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: 0.8,
|
||||
})),
|
||||
];
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
import type { Metadata } from "next";
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Terms and Conditions",
|
||||
title: 'Terms and Conditions',
|
||||
description:
|
||||
"Read the terms and conditions for using Dokploy's website and services.",
|
||||
};
|
||||
}
|
||||
|
||||
export default function TermsPage() {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full max-w-4xl mx-auto py-12 px-4">
|
||||
<h1 className="text-3xl font-bold text-center mb-6">
|
||||
<div className="mx-auto flex w-full max-w-4xl flex-col gap-4 px-4 py-12">
|
||||
<h1 className="mb-6 text-center text-3xl font-bold">
|
||||
Terms and Conditions
|
||||
</h1>
|
||||
|
||||
<section className="flex flex-col gap-2">
|
||||
<p>
|
||||
Welcome to Dokploy! These Terms and Conditions outline the rules and
|
||||
regulations for the use of Dokploy's website and services.
|
||||
Welcome to Dokploy! These Terms and Conditions outline the
|
||||
rules and regulations for the use of Dokploy's website and
|
||||
services.
|
||||
</p>
|
||||
<p>
|
||||
By accessing or using our services, you agree to be bound by the
|
||||
following terms. If you do not agree with these terms, please do not
|
||||
use our website or services.
|
||||
By accessing or using our services, you agree to be bound by
|
||||
the following terms. If you do not agree with these terms,
|
||||
please do not use our website or services.
|
||||
</p>
|
||||
<h2 className="text-2xl font-semibold mb-4">1. Definitions</h2>
|
||||
<h2 className="mb-4 text-2xl font-semibold">1. Definitions</h2>
|
||||
<p className="">
|
||||
Website: Refers to the website of Dokploy (
|
||||
<a
|
||||
@@ -35,171 +36,189 @@ export default function TermsPage() {
|
||||
) and its subdomains.
|
||||
</p>
|
||||
<p>
|
||||
Services: The platform and related services offered by Dokploy for
|
||||
deploying and managing applications using Docker and other related
|
||||
tools.
|
||||
Services: The platform and related services offered by
|
||||
Dokploy for deploying and managing applications using Docker
|
||||
and other related tools.
|
||||
</p>
|
||||
<p>User: Any individual or organization using Dokploy.</p>
|
||||
<p>
|
||||
Subscription: The paid plan for using additional features, resources,
|
||||
or server capacity.
|
||||
Subscription: The paid plan for using additional features,
|
||||
resources, or server capacity.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="">
|
||||
<h2 className="text-2xl font-semibold mb-4">2. Service Description</h2>
|
||||
<h2 className="mb-4 text-2xl font-semibold">
|
||||
2. Service Description
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
Dokploy is a platform that allows users to deploy and manage web
|
||||
applications on their own servers using custom builders and Docker
|
||||
technology. Dokploy offers both free and paid services, including
|
||||
subscriptions for adding additional servers, features, or increased
|
||||
capacity.
|
||||
Dokploy is a platform that allows users to deploy and manage
|
||||
web applications on their own servers using custom builders
|
||||
and Docker technology. Dokploy offers both free and paid
|
||||
services, including subscriptions for adding additional
|
||||
servers, features, or increased capacity.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-2">
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
<h2 className="mb-4 text-2xl font-semibold">
|
||||
3. User Responsibilities
|
||||
</h2>
|
||||
<p className="">
|
||||
Users are responsible for maintaining the security of their accounts,
|
||||
servers, and applications deployed through Dokploy.
|
||||
Users are responsible for maintaining the security of their
|
||||
accounts, servers, and applications deployed through
|
||||
Dokploy.
|
||||
</p>
|
||||
<p className="">
|
||||
Users must not use the platform for illegal activities, including but
|
||||
not limited to distributing malware, violating intellectual property
|
||||
rights, or engaging in cyberattacks.
|
||||
Users must not use the platform for illegal activities,
|
||||
including but not limited to distributing malware, violating
|
||||
intellectual property rights, or engaging in cyberattacks.
|
||||
</p>
|
||||
<p className="">
|
||||
Users must comply with all local, state, and international laws in
|
||||
connection with their use of Dokploy.
|
||||
Users must comply with all local, state, and international
|
||||
laws in connection with their use of Dokploy.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="">
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
<h2 className="mb-4 text-2xl font-semibold">
|
||||
4. Subscription and Payment
|
||||
</h2>
|
||||
<ul className="list-disc list-inside mb-4">
|
||||
<ul className="mb-4 list-inside list-disc">
|
||||
<li>
|
||||
By purchasing a subscription, users agree to the pricing and payment
|
||||
terms detailed on the website or via Paddle (our payment processor).
|
||||
By purchasing a subscription, users agree to the pricing
|
||||
and payment terms detailed on the website or via Paddle
|
||||
(our payment processor).
|
||||
</li>
|
||||
<li>
|
||||
Subscriptions renew automatically unless canceled by the user before
|
||||
the renewal date.
|
||||
Subscriptions renew automatically unless canceled by the
|
||||
user before the renewal date.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="">
|
||||
<h2 className="text-2xl font-semibold mb-4">5. Refund Policy</h2>
|
||||
<h2 className="mb-4 text-2xl font-semibold">
|
||||
5. Refund Policy
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
Due to the nature of our digital services, Dokploy operates on a
|
||||
no-refund policy for any paid subscriptions, except where required by
|
||||
law. We offer a self-hosted version of Dokploy with the same core
|
||||
functionalities, which users can deploy and use without any cost. We
|
||||
recommend users try the self-hosted version to evaluate the platform
|
||||
before committing to a paid subscription.
|
||||
Due to the nature of our digital services, Dokploy operates
|
||||
on a no-refund policy for any paid subscriptions, except
|
||||
where required by law. We offer a self-hosted version of
|
||||
Dokploy with the same core functionalities, which users can
|
||||
deploy and use without any cost. We recommend users try the
|
||||
self-hosted version to evaluate the platform before
|
||||
committing to a paid subscription.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-2">
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
<h2 className="mb-4 text-2xl font-semibold">
|
||||
6. Limitations of Liability
|
||||
</h2>
|
||||
<p className="">
|
||||
Dokploy is provided "as is" without any warranties, express or
|
||||
implied, including but not limited to the availability, reliability,
|
||||
or accuracy of the service.
|
||||
Dokploy is provided "as is" without any warranties, express
|
||||
or implied, including but not limited to the availability,
|
||||
reliability, or accuracy of the service.
|
||||
</p>
|
||||
<p className="">
|
||||
Users are fully responsible for any modifications made to their remote
|
||||
servers or the environment where Dokploy is deployed. Any changes to
|
||||
the server configuration, system settings, security policies, or other
|
||||
environments that deviate from the recommended use of Dokploy may
|
||||
result in compatibility issues, performance degradation, or security
|
||||
vulnerabilities. Additionally, Dokploy may not function properly on
|
||||
unsupported operating systems or environments. We do not guarantee the
|
||||
platform will operate correctly or reliably under modified server
|
||||
conditions or on unsupported systems, and we will not be held liable
|
||||
for any disruptions, malfunctions, or damages resulting from such
|
||||
changes or unsupported configurations.
|
||||
Users are fully responsible for any modifications made to
|
||||
their remote servers or the environment where Dokploy is
|
||||
deployed. Any changes to the server configuration, system
|
||||
settings, security policies, or other environments that
|
||||
deviate from the recommended use of Dokploy may result in
|
||||
compatibility issues, performance degradation, or security
|
||||
vulnerabilities. Additionally, Dokploy may not function
|
||||
properly on unsupported operating systems or environments.
|
||||
We do not guarantee the platform will operate correctly or
|
||||
reliably under modified server conditions or on unsupported
|
||||
systems, and we will not be held liable for any disruptions,
|
||||
malfunctions, or damages resulting from such changes or
|
||||
unsupported configurations.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="">
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
<h2 className="mb-4 text-2xl font-semibold">
|
||||
7. Service Modifications and Downtime
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
While we strive to provide uninterrupted service, there may be periods
|
||||
of downtime due to scheduled maintenance or upgrades to our
|
||||
infrastructure, such as server maintenance or system improvements. We
|
||||
will provide notice to users ahead of any planned maintenance.
|
||||
While we strive to provide uninterrupted service, there may
|
||||
be periods of downtime due to scheduled maintenance or
|
||||
upgrades to our infrastructure, such as server maintenance
|
||||
or system improvements. We will provide notice to users
|
||||
ahead of any planned maintenance.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-2">
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
<h2 className="mb-4 text-2xl font-semibold">
|
||||
8. Intellectual Property
|
||||
</h2>
|
||||
<p className="">
|
||||
Dokploy retains all intellectual property rights to the platform,
|
||||
including code, design, and content.
|
||||
Dokploy retains all intellectual property rights to the
|
||||
platform, including code, design, and content.
|
||||
</p>
|
||||
<p className="">
|
||||
Users are granted a limited, non-exclusive, and non-transferable
|
||||
license to use Dokploy in accordance with these terms.
|
||||
Users are granted a limited, non-exclusive, and
|
||||
non-transferable license to use Dokploy in accordance with
|
||||
these terms.
|
||||
</p>
|
||||
<p className="">
|
||||
Users may not modify, reverse-engineer, or distribute any part of the
|
||||
platform without express permission.
|
||||
Users may not modify, reverse-engineer, or distribute any
|
||||
part of the platform without express permission.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="">
|
||||
<h2 className="text-2xl font-semibold mb-4">9. Termination</h2>
|
||||
<h2 className="mb-4 text-2xl font-semibold">9. Termination</h2>
|
||||
<p className="mb-4">
|
||||
Dokploy reserves the right to suspend or terminate access to the
|
||||
platform for users who violate these terms or engage in harmful
|
||||
behavior.
|
||||
Dokploy reserves the right to suspend or terminate access to
|
||||
the platform for users who violate these terms or engage in
|
||||
harmful behavior.
|
||||
</p>
|
||||
<p className="mb-4">
|
||||
Users may terminate their account at any time by contacting support.
|
||||
Upon termination, access to the platform will be revoked, and any
|
||||
stored data may be permanently deleted.
|
||||
Users may terminate their account at any time by contacting
|
||||
support. Upon termination, access to the platform will be
|
||||
revoked, and any stored data may be permanently deleted.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="">
|
||||
<h2 className="text-2xl font-semibold mb-4">10. Changes to Terms</h2>
|
||||
<h2 className="mb-4 text-2xl font-semibold">
|
||||
10. Changes to Terms
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
Dokploy reserves the right to update these Terms & Conditions at any
|
||||
time. Changes will be effective immediately upon posting on the
|
||||
website. It is the user's responsibility to review these terms
|
||||
periodically.
|
||||
Dokploy reserves the right to update these Terms &
|
||||
Conditions at any time. Changes will be effective
|
||||
immediately upon posting on the website. It is the user's
|
||||
responsibility to review these terms periodically.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="">
|
||||
<h2 className="text-2xl font-semibold mb-4">11. Governing Law</h2>
|
||||
<h2 className="mb-4 text-2xl font-semibold">
|
||||
11. Governing Law
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
These Terms & Conditions are governed by applicable laws based on the
|
||||
user's location. Any disputes arising under these terms will be
|
||||
resolved in accordance with the legal jurisdiction relevant to the
|
||||
user's location, unless otherwise required by applicable law.
|
||||
These Terms & Conditions are governed by applicable laws
|
||||
based on the user's location. Any disputes arising under
|
||||
these terms will be resolved in accordance with the legal
|
||||
jurisdiction relevant to the user's location, unless
|
||||
otherwise required by applicable law.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="">
|
||||
<h2 className="text-2xl font-semibold mb-4">12. Contact Information</h2>
|
||||
<h2 className="mb-4 text-2xl font-semibold">
|
||||
12. Contact Information
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
If you have any questions or concerns regarding these Terms, you can
|
||||
reach us at:
|
||||
If you have any questions or concerns regarding these Terms,
|
||||
you can reach us at:
|
||||
</p>
|
||||
<p className="mb-4">
|
||||
Email:{" "}
|
||||
Email:{' '}
|
||||
<a
|
||||
href="mailto:support@dokploy.com"
|
||||
className="text-blue-500 hover:underline"
|
||||
@@ -209,6 +228,5 @@ export default function TermsPage() {
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,13 +1,16 @@
|
||||
import clsx from "clsx";
|
||||
import clsx from 'clsx'
|
||||
|
||||
export function Container({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<"div">) {
|
||||
}: React.ComponentPropsWithoutRef<'div'>) {
|
||||
return (
|
||||
<div
|
||||
className={clsx("mx-auto max-w-7xl px-4 sm:px-6 lg:px-8", className)}
|
||||
className={clsx(
|
||||
'mx-auto max-w-7xl px-4 sm:px-6 lg:px-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,79 +3,80 @@ import {
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Container } from "./Container";
|
||||
} from '@/components/ui/accordion'
|
||||
import { Container } from './Container'
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "What is Dokploy?",
|
||||
answer: "Dokploy is a stable, easy-to-use deployment solution designed to simplify the application management process. Think of Dokploy as a free alternative self-hostable solution to platforms like Heroku, Vercel, and Netlify.",
|
||||
question: 'What is Dokploy?',
|
||||
answer: 'Dokploy is a stable, easy-to-use deployment solution designed to simplify the application management process. Think of Dokploy as a free alternative self-hostable solution to platforms like Heroku, Vercel, and Netlify.',
|
||||
},
|
||||
{
|
||||
question: "How does Dokploy's Open Source plan work?",
|
||||
answer: "You can host Dokploy UI on your own infrastructure and you will be responsible for the maintenance and updates.",
|
||||
answer: 'You can host Dokploy UI on your own infrastructure and you will be responsible for the maintenance and updates.',
|
||||
},
|
||||
{
|
||||
question: "Do I need to provide my own server for the managed plan?",
|
||||
answer: "Yes, in the managed plan, you provide your own server (e.g., Hetzner, Hostinger, AWS, etc.) VPS, and we manage the Dokploy UI infrastructure for you.",
|
||||
question: 'Do I need to provide my own server for the managed plan?',
|
||||
answer: 'Yes, in the managed plan, you provide your own server (e.g., Hetzner, Hostinger, AWS, etc.) VPS, and we manage the Dokploy UI infrastructure for you.',
|
||||
},
|
||||
{
|
||||
question: "What happens if I need more than one server?",
|
||||
answer: "The first server costs $4.50/month, if you buy more than one it will be $3.50/month per server.",
|
||||
question: 'What happens if I need more than one server?',
|
||||
answer: 'The first server costs $4.50/month, if you buy more than one it will be $3.50/month per server.',
|
||||
},
|
||||
{
|
||||
question: "Is there a limit on the number of deployments?",
|
||||
answer: "No, there is no limit on the number of deployments in any of the plans.",
|
||||
question: 'Is there a limit on the number of deployments?',
|
||||
answer: 'No, there is no limit on the number of deployments in any of the plans.',
|
||||
},
|
||||
{
|
||||
question: "What happens if I exceed my purchased server limit?",
|
||||
question: 'What happens if I exceed my purchased server limit?',
|
||||
answer: "The most recently added servers will be deactivated. You won't be able to create services on inactive servers until they are reactivated.",
|
||||
},
|
||||
{
|
||||
question: "What kind of support do you offer?",
|
||||
answer: "We offer community support for the open source version and priority support for paid plans (via Discord or Email at support@dokploy.com).",
|
||||
question: 'What kind of support do you offer?',
|
||||
answer: 'We offer community support for the open source version and priority support for paid plans (via Discord or Email at support@dokploy.com).',
|
||||
},
|
||||
{
|
||||
question: "What's the catch on the Paid Plan?",
|
||||
answer: "Nothing, once you link your server (VPS) to your account, you can deploy unlimited applications, databases, and users, and you get unlimited updates, deployments, backups, and more.",
|
||||
answer: 'Nothing, once you link your server (VPS) to your account, you can deploy unlimited applications, databases, and users, and you get unlimited updates, deployments, backups, and more.',
|
||||
},
|
||||
{
|
||||
question: "Why Choose Dokploy?",
|
||||
answer: "Dokploy offers simplicity, flexibility, and speed in application deployment and management.",
|
||||
question: 'Why Choose Dokploy?',
|
||||
answer: 'Dokploy offers simplicity, flexibility, and speed in application deployment and management.',
|
||||
},
|
||||
{
|
||||
question: "Is it open source?",
|
||||
answer: "Yes, Dokploy is open source and free to use.",
|
||||
question: 'Is it open source?',
|
||||
answer: 'Yes, Dokploy is open source and free to use.',
|
||||
},
|
||||
{
|
||||
question: "What types of languages can I deploy with Dokploy?",
|
||||
answer: "Dokploy does not restrict programming languages. You are free to choose your preferred language and framework.",
|
||||
question: 'What types of languages can I deploy with Dokploy?',
|
||||
answer: 'Dokploy does not restrict programming languages. You are free to choose your preferred language and framework.',
|
||||
},
|
||||
{
|
||||
question: "How do I request a feature or report a bug?",
|
||||
answer: "To request a feature or report a bug, please create an issue on our GitHub repository or ask in our Discord channel.",
|
||||
question: 'How do I request a feature or report a bug?',
|
||||
answer: 'To request a feature or report a bug, please create an issue on our GitHub repository or ask in our Discord channel.',
|
||||
},
|
||||
{
|
||||
question: "Do you track the usage of Dokploy?",
|
||||
question: 'Do you track the usage of Dokploy?',
|
||||
answer: "No, we don't track any usage data.",
|
||||
},
|
||||
{
|
||||
question: "Are there any user forums or communities where I can interact with other users?",
|
||||
answer: "Yes, we have active GitHub discussions and Discord where you can share ideas, ask for help, and connect with other users.",
|
||||
question:
|
||||
'Are there any user forums or communities where I can interact with other users?',
|
||||
answer: 'Yes, we have active GitHub discussions and Discord where you can share ideas, ask for help, and connect with other users.',
|
||||
},
|
||||
{
|
||||
question: "Do you offer a refunds?",
|
||||
answer: "We do not offer refunds. However, you can cancel your subscription at any time. Feel free to try our open-source version for free before making a purchase.",
|
||||
question: 'Do you offer a refunds?',
|
||||
answer: 'We do not offer refunds. However, you can cancel your subscription at any time. Feel free to try our open-source version for free before making a purchase.',
|
||||
},
|
||||
{
|
||||
question: "What types of applications can I deploy with Dokploy?",
|
||||
answer: "You can deploy any application that can be Dockerized, with no limits. Dokploy supports builds from Git repositories, Dockerfiles, Nixpacks, and Buildpacks like Heroku and Paketo.",
|
||||
question: 'What types of applications can I deploy with Dokploy?',
|
||||
answer: 'You can deploy any application that can be Dockerized, with no limits. Dokploy supports builds from Git repositories, Dockerfiles, Nixpacks, and Buildpacks like Heroku and Paketo.',
|
||||
},
|
||||
{
|
||||
question: "How does Dokploy handle database management?",
|
||||
answer: "Dokploy supports multiple database systems including Postgres, MySQL, MariaDB, MongoDB, and Redis, providing tools for easy deployment and management and backups directly from the dashboard.",
|
||||
question: 'How does Dokploy handle database management?',
|
||||
answer: 'Dokploy supports multiple database systems including Postgres, MySQL, MariaDB, MongoDB, and Redis, providing tools for easy deployment and management and backups directly from the dashboard.',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
export function Faqs() {
|
||||
return (
|
||||
@@ -85,25 +86,30 @@ export function Faqs() {
|
||||
className="relative overflow-hidden bg-black py-20 sm:py-32"
|
||||
>
|
||||
<Container className="relative flex flex-col gap-10">
|
||||
<div className="mx-auto lg:mx-0 justify-center w-full">
|
||||
<div className="mx-auto w-full justify-center lg:mx-0">
|
||||
<h2
|
||||
id="faq-title"
|
||||
className="font-display text-3xl tracking-tight text-primary sm:text-4xl text-center"
|
||||
className="text-center font-display text-3xl tracking-tight text-primary sm:text-4xl"
|
||||
>
|
||||
Frequently asked questions
|
||||
</h2>
|
||||
<p className="mt-4 text-lg tracking-tight text-muted-foreground text-center">
|
||||
If you can't find what you're looking for, please submit an issue through our GitHub repository or ask questions on our Discord.
|
||||
<p className="mt-4 text-center text-lg tracking-tight text-muted-foreground">
|
||||
If you can't find what you're looking for, please submit
|
||||
an issue through our GitHub repository or ask questions
|
||||
on our Discord.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="w-full max-w-3xl mx-auto"
|
||||
className="mx-auto w-full max-w-3xl"
|
||||
>
|
||||
{faqs.map((faq, columnIndex) => (
|
||||
<AccordionItem value={`${columnIndex}`} key={columnIndex}>
|
||||
<AccordionItem
|
||||
value={`${columnIndex}`}
|
||||
key={columnIndex}
|
||||
>
|
||||
<AccordionTrigger className="text-left">
|
||||
{faq.question}
|
||||
</AccordionTrigger>
|
||||
@@ -113,5 +119,5 @@ export function Faqs() {
|
||||
</Accordion>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import Link from "next/link";
|
||||
import type { SVGProps } from "react";
|
||||
import { Container } from "./Container";
|
||||
import { NavLink } from "./NavLink";
|
||||
import { Logo } from "./shared/Logo";
|
||||
import { buttonVariants } from "./ui/button";
|
||||
import Link from 'next/link'
|
||||
import type { SVGProps } from 'react'
|
||||
import { Container } from './Container'
|
||||
import { NavLink } from './NavLink'
|
||||
import { Logo } from './shared/Logo'
|
||||
import { buttonVariants } from './ui/button'
|
||||
|
||||
const I18nIcon = (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
@@ -23,7 +23,7 @@ const I18nIcon = (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
||||
d="m478.33 433.6-90-218a22 22 0 0 0-40.67 0l-90 218a22 22 0 1 0 40.67 16.79L316.66 406h102.67l18.33 44.39A22 22 0 0 0 458 464a22 22 0 0 0 20.32-30.4zM334.83 362 368 281.65 401.17 362zm-66.99-19.08a22 22 0 0 0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73 39.65-53.68 62.11-114.75 71.27-143.49H330a22 22 0 0 0 0-44H214V70a22 22 0 0 0-44 0v20H54a22 22 0 0 0 0 44h197.25c-9.52 26.95-27.05 69.5-53.79 108.36-31.41-41.68-43.08-68.65-43.17-68.87a22 22 0 0 0-40.58 17c.58 1.38 14.55 34.23 52.86 83.93.92 1.19 1.83 2.35 2.74 3.51-39.24 44.35-77.74 71.86-93.85 80.74a22 22 0 1 0 21.07 38.63c2.16-1.18 48.6-26.89 101.63-85.59 22.52 24.08 38 35.44 38.93 36.1a22 22 0 0 0 30.75-4.9z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
)
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
@@ -51,7 +51,7 @@ export function Footer() {
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex flex-col items-center border-t border-slate-400/10 py-10 sm:flex-row-reverse sm:justify-between">
|
||||
<div className="flex gap-x-6 items-center">
|
||||
<div className="flex items-center gap-x-6">
|
||||
<Link
|
||||
href="https://x.com/getdokploy"
|
||||
className="group"
|
||||
@@ -87,5 +87,5 @@ export function Footer() {
|
||||
</div>
|
||||
</Container>
|
||||
</footer>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,77 +1,79 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type GithubStarsProps = {
|
||||
className?: string;
|
||||
repoUrl?: string;
|
||||
label?: string;
|
||||
count?: string;
|
||||
};
|
||||
className?: string
|
||||
repoUrl?: string
|
||||
label?: string
|
||||
count?: string
|
||||
}
|
||||
|
||||
// Function to format star count (e.g., 26400 -> "26.4k")
|
||||
function formatStarCount(count: number): string {
|
||||
if (count >= 1000000) {
|
||||
return `${(count / 1000000).toFixed(1)}M`;
|
||||
return `${(count / 1000000).toFixed(1)}M`
|
||||
}
|
||||
if (count >= 1000) {
|
||||
return `${(count / 1000).toFixed(1)}k`;
|
||||
return `${(count / 1000).toFixed(1)}k`
|
||||
}
|
||||
return count.toString();
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
// Extract owner and repo from GitHub URL
|
||||
function extractRepoInfo(url: string): { owner: string; repo: string } | null {
|
||||
try {
|
||||
const match = url.match(/github\.com\/([^\/]+)\/([^\/]+)/);
|
||||
const match = url.match(/github\.com\/([^\/]+)\/([^\/]+)/)
|
||||
if (match) {
|
||||
return { owner: match[1], repo: match[2].replace(/\.git$/, "") };
|
||||
return { owner: match[1], repo: match[2].replace(/\.git$/, '') }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error extracting repo info:", error);
|
||||
console.error('Error extracting repo info:', error)
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
export function GithubStars({
|
||||
className,
|
||||
repoUrl = "https://github.com/dokploy/dokploy",
|
||||
label = "GitHub Stars",
|
||||
count: defaultCount = "26.4k",
|
||||
repoUrl = 'https://github.com/dokploy/dokploy',
|
||||
label = 'GitHub Stars',
|
||||
count: defaultCount = '26.4k',
|
||||
}: GithubStarsProps) {
|
||||
const [starCount, setStarCount] = useState<string>(defaultCount);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [starCount, setStarCount] = useState<string>(defaultCount)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStarCount = async () => {
|
||||
const repoInfo = extractRepoInfo(repoUrl);
|
||||
const repoInfo = extractRepoInfo(repoUrl)
|
||||
if (!repoInfo) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/github-stars?owner=${encodeURIComponent(repoInfo.owner)}&repo=${encodeURIComponent(repoInfo.repo)}`,
|
||||
);
|
||||
)
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const formattedCount = formatStarCount(data.stargazers_count);
|
||||
setStarCount(formattedCount);
|
||||
const data = await response.json()
|
||||
const formattedCount = formatStarCount(
|
||||
data.stargazers_count,
|
||||
)
|
||||
setStarCount(formattedCount)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching GitHub stars:", error);
|
||||
console.error('Error fetching GitHub stars:', error)
|
||||
// Keep default count on error
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchStarCount();
|
||||
}, [repoUrl]);
|
||||
fetchStarCount()
|
||||
}, [repoUrl])
|
||||
|
||||
return (
|
||||
<Link
|
||||
@@ -79,11 +81,11 @@ export function GithubStars({
|
||||
target="_blank"
|
||||
aria-label={`${label}: ${starCount}`}
|
||||
className={cn(
|
||||
"group relative inline-flex items-center gap-2 rounded-full px-3 py-1",
|
||||
"shadow-[0_0_0_2px_#000_inset,0_2px_8px_rgba(0,0,0,0.35)]",
|
||||
"bg-gradient-to-b from-yellow-300 via-yellow-400 to-yellow-500",
|
||||
"text-black",
|
||||
"transition-transform hover:scale-[1.02] active:scale-[0.99]",
|
||||
'group relative inline-flex items-center gap-2 rounded-full px-3 py-1',
|
||||
'shadow-[0_0_0_2px_#000_inset,0_2px_8px_rgba(0,0,0,0.35)]',
|
||||
'bg-gradient-to-b from-yellow-300 via-yellow-400 to-yellow-500',
|
||||
'text-black',
|
||||
'transition-transform hover:scale-[1.02] active:scale-[0.99]',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -96,9 +98,9 @@ export function GithubStars({
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className={cn(
|
||||
"absolute -top-1 -left-1 h-3 w-3 text-yellow-100",
|
||||
"drop-shadow-[0_0_6px_rgba(255,255,200,0.9)]",
|
||||
"animate-pulse [animation-duration:1.6s] [animation-delay:.2s]",
|
||||
'absolute -left-1 -top-1 h-3 w-3 text-yellow-100',
|
||||
'drop-shadow-[0_0_6px_rgba(255,255,200,0.9)]',
|
||||
'animate-pulse [animation-delay:.2s] [animation-duration:1.6s]',
|
||||
)}
|
||||
fill="currentColor"
|
||||
>
|
||||
@@ -108,9 +110,9 @@ export function GithubStars({
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className={cn(
|
||||
"absolute -top-2 right-1 h-2.5 w-2.5 text-yellow-50",
|
||||
"drop-shadow-[0_0_6px_rgba(255,255,220,0.95)]",
|
||||
"animate-pulse [animation-duration:1.9s] [animation-delay:.7s]",
|
||||
'absolute -top-2 right-1 h-2.5 w-2.5 text-yellow-50',
|
||||
'drop-shadow-[0_0_6px_rgba(255,255,220,0.95)]',
|
||||
'animate-pulse [animation-delay:.7s] [animation-duration:1.9s]',
|
||||
)}
|
||||
fill="currentColor"
|
||||
>
|
||||
@@ -120,9 +122,9 @@ export function GithubStars({
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className={cn(
|
||||
"absolute -bottom-1 -right-1 h-3.5 w-3.5 text-yellow-200",
|
||||
"drop-shadow-[0_0_8px_rgba(255,255,180,0.85)]",
|
||||
"animate-pulse [animation-duration:2.2s] [animation-delay:1.1s]",
|
||||
'absolute -bottom-1 -right-1 h-3.5 w-3.5 text-yellow-200',
|
||||
'drop-shadow-[0_0_8px_rgba(255,255,180,0.85)]',
|
||||
'animate-pulse [animation-delay:1.1s] [animation-duration:2.2s]',
|
||||
)}
|
||||
fill="currentColor"
|
||||
>
|
||||
@@ -137,10 +139,10 @@ export function GithubStars({
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"absolute -inset-x-10 -top-6 h-10 rotate-12",
|
||||
"bg-white/40 blur-md",
|
||||
"opacity-0 transition-opacity duration-500",
|
||||
"group-hover:opacity-40",
|
||||
'absolute -inset-x-10 -top-6 h-10 rotate-12',
|
||||
'bg-white/40 blur-md',
|
||||
'opacity-0 transition-opacity duration-500',
|
||||
'group-hover:opacity-40',
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
@@ -148,9 +150,9 @@ export function GithubStars({
|
||||
{/* GitHub mark */}
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-6 w-6 items-center justify-center rounded-full",
|
||||
"bg-black text-white",
|
||||
"shadow-[inset_0_0_0_1px_rgba(255,255,255,0.15)]",
|
||||
'flex h-6 w-6 items-center justify-center rounded-full',
|
||||
'bg-black text-white',
|
||||
'shadow-[inset_0_0_0_1px_rgba(255,255,255,0.15)]',
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
@@ -167,7 +169,7 @@ export function GithubStars({
|
||||
<span className="flex items-baseline gap-1 pr-0.5">
|
||||
<span className="text-xs font-semibold">Stars</span>
|
||||
<span className="text-sm font-extrabold tracking-tight">
|
||||
{isLoading ? "..." : starCount}
|
||||
{isLoading ? '...' : starCount}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -175,12 +177,12 @@ export function GithubStars({
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 rounded-full",
|
||||
"ring-1 ring-black/10 group-hover:ring-black/20",
|
||||
'pointer-events-none absolute inset-0 rounded-full',
|
||||
'ring-1 ring-black/10 group-hover:ring-black/20',
|
||||
)}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default GithubStars;
|
||||
export default GithubStars
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { ChevronRight, HeartIcon } from "lucide-react";
|
||||
import { Fragment, type JSX, type SVGProps } from "react";
|
||||
import { Container } from "./Container";
|
||||
import { NavLink } from "./NavLink";
|
||||
import { trackGAEvent } from "./analitycs";
|
||||
import { Logo } from "./shared/Logo";
|
||||
import AnimatedGradientText from "./ui/animated-gradient-text";
|
||||
import { Button, buttonVariants } from "./ui/button";
|
||||
import GithubStars from "./GithubStars";
|
||||
import Link from 'next/link'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { ChevronRight, HeartIcon } from 'lucide-react'
|
||||
import { Fragment, type JSX, type SVGProps } from 'react'
|
||||
import { Container } from './Container'
|
||||
import { NavLink } from './NavLink'
|
||||
import { trackGAEvent } from './analitycs'
|
||||
import { Logo } from './shared/Logo'
|
||||
import AnimatedGradientText from './ui/animated-gradient-text'
|
||||
import { Button, buttonVariants } from './ui/button'
|
||||
import GithubStars from './GithubStars'
|
||||
|
||||
function MobileNavLink({
|
||||
href,
|
||||
children,
|
||||
target,
|
||||
}: {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
target?: string;
|
||||
href: string
|
||||
children: React.ReactNode
|
||||
target?: string
|
||||
}) {
|
||||
return (
|
||||
<Popover.Button
|
||||
onClick={() => {
|
||||
trackGAEvent({
|
||||
action: "Nav Link Clicked",
|
||||
category: "Navigation",
|
||||
action: 'Nav Link Clicked',
|
||||
category: 'Navigation',
|
||||
label: href,
|
||||
});
|
||||
})
|
||||
}}
|
||||
as={Link}
|
||||
href={href}
|
||||
@@ -38,7 +38,7 @@ function MobileNavLink({
|
||||
>
|
||||
{children}
|
||||
</Popover.Button>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function MobileNavIcon({ open }: { open: boolean }) {
|
||||
@@ -52,17 +52,20 @@ function MobileNavIcon({ open }: { open: boolean }) {
|
||||
>
|
||||
<path
|
||||
d="M0 1H14M0 7H14M0 13H14"
|
||||
className={cn("origin-center transition", open && "scale-90 opacity-0")}
|
||||
className={cn(
|
||||
'origin-center transition',
|
||||
open && 'scale-90 opacity-0',
|
||||
)}
|
||||
/>
|
||||
<path
|
||||
d="M2 2L12 12M12 2L2 12"
|
||||
className={cn(
|
||||
"origin-center transition",
|
||||
!open && "scale-90 opacity-0",
|
||||
'origin-center transition',
|
||||
!open && 'scale-90 opacity-0',
|
||||
)}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const I18nIcon = (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
||||
@@ -81,7 +84,7 @@ const I18nIcon = (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
||||
d="m478.33 433.6-90-218a22 22 0 0 0-40.67 0l-90 218a22 22 0 1 0 40.67 16.79L316.66 406h102.67l18.33 44.39A22 22 0 0 0 458 464a22 22 0 0 0 20.32-30.4zM334.83 362 368 281.65 401.17 362zm-66.99-19.08a22 22 0 0 0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73 39.65-53.68 62.11-114.75 71.27-143.49H330a22 22 0 0 0 0-44H214V70a22 22 0 0 0-44 0v20H54a22 22 0 0 0 0 44h197.25c-9.52 26.95-27.05 69.5-53.79 108.36-31.41-41.68-43.08-68.65-43.17-68.87a22 22 0 0 0-40.58 17c.58 1.38 14.55 34.23 52.86 83.93.92 1.19 1.83 2.35 2.74 3.51-39.24 44.35-77.74 71.86-93.85 80.74a22 22 0 1 0 21.07 38.63c2.16-1.18 48.6-26.89 101.63-85.59 22.52 24.08 38 35.44 38.93 36.1a22 22 0 0 0 30.75-4.9z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
)
|
||||
|
||||
function MobileNavigation() {
|
||||
return (
|
||||
@@ -138,7 +141,7 @@ function MobileNavigation() {
|
||||
aria-label="Sign In Dokploy Cloud"
|
||||
target="_blank"
|
||||
>
|
||||
<div className="group flex-row relative mx-auto flex max-w-fit items-center justify-center rounded-2xl text-sm font-medium w-full">
|
||||
<div className="group relative mx-auto flex w-full max-w-fit flex-row items-center justify-center rounded-2xl text-sm font-medium">
|
||||
<span>Sign In</span>
|
||||
<ChevronRight className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
|
||||
</div>
|
||||
@@ -149,12 +152,12 @@ function MobileNavigation() {
|
||||
</Transition.Child>
|
||||
</Transition.Root>
|
||||
</Popover>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b border-border/40 py-5">
|
||||
<header className="sticky top-0 z-50 border-b border-border/40 bg-background/95 py-5 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<Container>
|
||||
<nav className="relative z-50 flex justify-between">
|
||||
<div className="flex items-center md:gap-x-12">
|
||||
@@ -183,7 +186,7 @@ export function Header() {
|
||||
strokeWidth="0"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5 fill-muted-foreground group-hover:fill-muted-foreground/70 hover:fill-muted-foreground/80"
|
||||
className="h-5 w-5 fill-muted-foreground hover:fill-muted-foreground/80 group-hover:fill-muted-foreground/70"
|
||||
>
|
||||
<path d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z" />
|
||||
</svg>
|
||||
@@ -198,10 +201,10 @@ export function Header() {
|
||||
href="/contact"
|
||||
onClick={() => {
|
||||
trackGAEvent({
|
||||
action: "Contact Button Clicked",
|
||||
category: "Contact",
|
||||
label: "Header",
|
||||
});
|
||||
action: 'Contact Button Clicked',
|
||||
category: 'Contact',
|
||||
label: 'Header',
|
||||
})
|
||||
}}
|
||||
>
|
||||
Contact
|
||||
@@ -228,7 +231,7 @@ export function Header() {
|
||||
aria-label="Sign In Dokploy Cloud"
|
||||
target="_blank"
|
||||
>
|
||||
<div className="group flex-row relative mx-auto flex max-w-fit items-center justify-center rounded-2xl text-sm font-medium w-full">
|
||||
<div className="group relative mx-auto flex w-full max-w-fit flex-row items-center justify-center rounded-2xl text-sm font-medium">
|
||||
<span>Sign In</span>
|
||||
<ChevronRight className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
|
||||
</div>
|
||||
@@ -241,5 +244,5 @@ export function Header() {
|
||||
</nav>
|
||||
</Container>
|
||||
</header>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion } from "framer-motion";
|
||||
import { Check, ChevronRight, Copy } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import AnimatedGradientText from "./ui/animated-gradient-text";
|
||||
import AnimatedGridPattern from "./ui/animated-grid-pattern";
|
||||
import { Button } from "./ui/button";
|
||||
import HeroVideoDialog from "./ui/hero-video-dialog";
|
||||
'use client'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { motion } from 'framer-motion'
|
||||
import { Check, ChevronRight, Copy } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import AnimatedGradientText from './ui/animated-gradient-text'
|
||||
import AnimatedGridPattern from './ui/animated-grid-pattern'
|
||||
import { Button } from './ui/button'
|
||||
import HeroVideoDialog from './ui/hero-video-dialog'
|
||||
|
||||
// const ProductHunt = () => {
|
||||
// return (
|
||||
@@ -42,14 +42,14 @@ import HeroVideoDialog from "./ui/hero-video-dialog";
|
||||
// };
|
||||
|
||||
export function Hero() {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [isCopied]);
|
||||
setIsCopied(false)
|
||||
}, 2000)
|
||||
return () => clearTimeout(timer)
|
||||
}, [isCopied])
|
||||
return (
|
||||
<div className="h-[1100px] bg-black pt-20 sm:h-[1100px] lg:pt-32">
|
||||
<div className=" bottom-0 flex w-full items-center justify-center overflow-hidden rounded-lg bg-background md:shadow-xl">
|
||||
@@ -83,7 +83,7 @@ export function Hero() {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
Simplify{" "}
|
||||
Simplify{' '}
|
||||
<span className="relative whitespace-nowrap text-primary">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
@@ -93,8 +93,10 @@ export function Hero() {
|
||||
>
|
||||
<path d="M203.371.916c-26.013-2.078-76.686 1.963-124.73 9.946L67.3 12.749C35.421 18.062 18.2 21.766 6.004 25.934 1.244 27.561.828 27.778.874 28.61c.07 1.214.828 1.121 9.595-1.176 9.072-2.377 17.15-3.92 39.246-7.496C123.565 7.986 157.869 4.492 195.942 5.046c7.461.108 19.25 1.696 19.17 2.582-.107 1.183-7.874 4.31-25.75 10.366-21.992 7.45-35.43 12.534-36.701 13.884-2.173 2.308-.202 4.407 4.442 4.734 2.654.187 3.263.157 15.593-.78 35.401-2.686 57.944-3.488 88.365-3.143 46.327.526 75.721 2.23 130.788 7.584 19.787 1.924 20.814 1.98 24.557 1.332l.066-.011c1.201-.203 1.53-1.825.399-2.335-2.911-1.31-4.893-1.604-22.048-3.261-57.509-5.556-87.871-7.36-132.059-7.842-23.239-.254-33.617-.116-50.627.674-11.629.54-42.371 2.494-46.696 2.967-2.359.259 8.133-3.625 26.504-9.81 23.239-7.825 27.934-10.149 28.304-14.005.417-4.348-3.529-6-16.878-7.066Z" />
|
||||
</svg>
|
||||
<span className="relative">Application and Database</span>
|
||||
</span>{" "}
|
||||
<span className="relative">
|
||||
Application and Database
|
||||
</span>
|
||||
</span>{' '}
|
||||
Deployments
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
@@ -103,7 +105,9 @@ export function Hero() {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.2 }}
|
||||
>
|
||||
Manage containerized deployments across multiple servers with ease thanks to our all-in-one platform for developers.
|
||||
Manage containerized deployments across multiple
|
||||
servers with ease thanks to our all-in-one platform
|
||||
for developers.
|
||||
</motion.p>
|
||||
<motion.div
|
||||
className="flex flex-col items-center justify-center space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0"
|
||||
@@ -114,16 +118,21 @@ export function Hero() {
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="mt-6 flex flex-wrap items-center justify-center gap-6 md:flex-nowrap">
|
||||
<code className="flex flex-row items-center gap-4 rounded-xl border p-3 font-sans">
|
||||
curl -sSL https://dokploy.com/install.sh | sh
|
||||
curl -sSL https://dokploy.com/install.sh
|
||||
| sh
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
navigator.clipboard
|
||||
.writeText(
|
||||
"curl -sSL https://dokploy.com/install.sh | sh",
|
||||
'curl -sSL https://dokploy.com/install.sh | sh',
|
||||
)
|
||||
.then(() =>
|
||||
setIsCopied(true),
|
||||
)
|
||||
.catch(() =>
|
||||
setIsCopied(false),
|
||||
)
|
||||
.then(() => setIsCopied(true))
|
||||
.catch(() => setIsCopied(false))
|
||||
}
|
||||
>
|
||||
{isCopied ? (
|
||||
@@ -135,14 +144,20 @@ export function Hero() {
|
||||
</code>
|
||||
</div>
|
||||
<div className="mx-auto flex w-full max-w-sm flex-wrap items-center justify-center gap-3 md:flex-nowrap">
|
||||
<Button className="w-full rounded-full" asChild>
|
||||
<Button
|
||||
className="w-full rounded-full"
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
href="https://github.com/dokploy/dokploy"
|
||||
aria-label="Dokploy on GitHub"
|
||||
target="_blank"
|
||||
className="flex flex-row items-center gap-2"
|
||||
>
|
||||
<svg aria-hidden="true" className="h-6 w-6 fill-black">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="h-6 w-6 fill-black"
|
||||
>
|
||||
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2Z" />
|
||||
</svg>
|
||||
GitHub
|
||||
@@ -198,11 +213,11 @@ export function Hero() {
|
||||
duration={3}
|
||||
repeatDelay={1}
|
||||
className={cn(
|
||||
"[mask-image:radial-gradient(800px_circle_at_center,white,transparent)]",
|
||||
"absolute inset-x-0 inset-y-[-30%] h-[200%] skew-y-12",
|
||||
'[mask-image:radial-gradient(800px_circle_at_center,white,transparent)]',
|
||||
'absolute inset-x-0 inset-y-[-30%] h-[200%] skew-y-12',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import Link from "next/link";
|
||||
import { trackGAEvent } from "./analitycs";
|
||||
import Link from 'next/link'
|
||||
import { trackGAEvent } from './analitycs'
|
||||
|
||||
export function NavLink({
|
||||
href,
|
||||
children,
|
||||
target,
|
||||
}: {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
target?: string;
|
||||
href: string
|
||||
children: React.ReactNode
|
||||
target?: string
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
@@ -18,16 +18,16 @@ export function NavLink({
|
||||
href={href}
|
||||
onClick={() =>
|
||||
trackGAEvent({
|
||||
action: "Nav Link Clicked",
|
||||
category: "Navigation",
|
||||
action: 'Nav Link Clicked',
|
||||
category: 'Navigation',
|
||||
label: href,
|
||||
})
|
||||
}
|
||||
target={target}
|
||||
className="inline-block self-center rounded-lg px-2.5 py-1.5 text-sm text-popover-foreground font-medium transition-colors hover:text-primary hover:bg-secondary"
|
||||
className="inline-block self-center rounded-lg px-2.5 py-1.5 text-sm font-medium text-popover-foreground transition-colors hover:bg-secondary hover:text-primary"
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,37 +1,40 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Layers, Terminal, Users } from "lucide-react";
|
||||
import { Container } from "./Container";
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Tab } from '@headlessui/react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { Layers, Terminal, Users } from 'lucide-react'
|
||||
import { Container } from './Container'
|
||||
interface Feature {
|
||||
name: string;
|
||||
summary: string;
|
||||
description: string;
|
||||
image: string;
|
||||
icon: React.ComponentType;
|
||||
name: string
|
||||
summary: string
|
||||
description: string
|
||||
image: string
|
||||
icon: React.ComponentType
|
||||
}
|
||||
|
||||
const features: Array<Feature> = [
|
||||
{
|
||||
name: "Open Source Templates",
|
||||
summary: "One click to deploy open source templates.",
|
||||
description: "Deploy open source templates with one click, powered by Docker Compose, (Plausible, Calcom, Pocketbase, etc.)",
|
||||
image: "/secondary/templates.png",
|
||||
name: 'Open Source Templates',
|
||||
summary: 'One click to deploy open source templates.',
|
||||
description:
|
||||
'Deploy open source templates with one click, powered by Docker Compose, (Plausible, Calcom, Pocketbase, etc.)',
|
||||
image: '/secondary/templates.png',
|
||||
icon: function ReportingIcon() {
|
||||
return (
|
||||
<>
|
||||
<Layers className="size-5 text-primary" />
|
||||
</>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Real-Time Traefik Configuration",
|
||||
summary: "Modify Traefik settings on-the-fly via a graphical interface or API.",
|
||||
description: "Users can adjust Traefik's configuration, including middleware, forwarding rules, and SSL certificates through an intuitive interface or API. This feature enables seamless traffic routing and security adjustments without the need to restart services",
|
||||
image: "/secondary/traefik.png",
|
||||
name: 'Real-Time Traefik Configuration',
|
||||
summary:
|
||||
'Modify Traefik settings on-the-fly via a graphical interface or API.',
|
||||
description:
|
||||
"Users can adjust Traefik's configuration, including middleware, forwarding rules, and SSL certificates through an intuitive interface or API. This feature enables seamless traffic routing and security adjustments without the need to restart services",
|
||||
image: '/secondary/traefik.png',
|
||||
icon: function ReportingIcon() {
|
||||
return (
|
||||
<>
|
||||
@@ -81,7 +84,12 @@ const features: Array<Feature> = [
|
||||
d="M299.847 285.567c10.027 58.288 105.304 42.877 91.619-15.91-12.271-52.716-94.951-38.124-91.619 15.91m-113.855 9.427c12.996 50.745 94.24 37.753 91.178-13.149-3.669-60.964-103.603-49.2-91.178 13.149m132.351 58.517c.044 7.79 1.843 15.403.289 24.148-1.935 3.656-5.729 4.043-9.001 5.52-4.524-.71-8.328-3.68-10.143-7.912-1.161-9.202.433-18.111.726-27.316l18.129 5.56z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<ellipse cx="208.4" cy="286.718" rx="13.719" ry="14.86" />
|
||||
<ellipse
|
||||
cx="208.4"
|
||||
cy="286.718"
|
||||
rx="13.719"
|
||||
ry="14.86"
|
||||
/>
|
||||
<ellipse
|
||||
cx="214.64"
|
||||
cy="290.071"
|
||||
@@ -89,9 +97,19 @@ const features: Array<Feature> = [
|
||||
ry="3.777"
|
||||
fill="#fff"
|
||||
/>
|
||||
<ellipse cx="323.348" cy="283.017" rx="13.491" ry="14.86" />
|
||||
<ellipse
|
||||
cx="323.348"
|
||||
cy="283.017"
|
||||
rx="13.491"
|
||||
ry="14.86"
|
||||
/>
|
||||
<g fill="#fff">
|
||||
<ellipse cx="329.485" cy="286.371" rx="3.181" ry="3.777" />
|
||||
<ellipse
|
||||
cx="329.485"
|
||||
cy="286.371"
|
||||
rx="3.181"
|
||||
ry="3.777"
|
||||
/>
|
||||
<path d="M279.137 354.685c-5.986 14.507 3.338 43.515 19.579 22.119-1.161-9.202.433-18.111.726-27.316l-20.305 5.197z" />
|
||||
</g>
|
||||
<path
|
||||
@@ -210,59 +228,63 @@ const features: Array<Feature> = [
|
||||
</g>
|
||||
</svg>
|
||||
</>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "User Permission Management",
|
||||
summary: "Detailed control over user permissions for accessing and managing projects and services.",
|
||||
description: "Allows administrators to define specific roles and permissions for each user, including the ability to create, modify, or delete applications and databases. This feature ensures secure and efficient management of large and diverse teams.",
|
||||
image: "/secondary/users.png",
|
||||
name: 'User Permission Management',
|
||||
summary:
|
||||
'Detailed control over user permissions for accessing and managing projects and services.',
|
||||
description:
|
||||
'Allows administrators to define specific roles and permissions for each user, including the ability to create, modify, or delete applications and databases. This feature ensures secure and efficient management of large and diverse teams.',
|
||||
image: '/secondary/users.png',
|
||||
icon: function InventoryIcon() {
|
||||
return (
|
||||
<>
|
||||
<Users className="size-5 text-primary" />
|
||||
</>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Terminal Access",
|
||||
summary: "Direct access to each container's and server terminal for advanced management.",
|
||||
description: "Provides an interface to access the command line of any active container, allowing developers to execute commands, manage services, and troubleshoot directly from the dashboard",
|
||||
image: "/secondary/terminal.png",
|
||||
name: 'Terminal Access',
|
||||
summary:
|
||||
"Direct access to each container's and server terminal for advanced management.",
|
||||
description:
|
||||
'Provides an interface to access the command line of any active container, allowing developers to execute commands, manage services, and troubleshoot directly from the dashboard',
|
||||
image: '/secondary/terminal.png',
|
||||
icon: function ContactsIcon() {
|
||||
return (
|
||||
<>
|
||||
<Terminal className="size-5 text-primary" />
|
||||
</>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
function Feature({
|
||||
feature,
|
||||
isActive,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<"div"> & {
|
||||
feature: Feature;
|
||||
isActive: boolean;
|
||||
}: React.ComponentPropsWithoutRef<'div'> & {
|
||||
feature: Feature
|
||||
isActive: boolean
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
className,
|
||||
!isActive ? "opacity-75 hover:opacity-100 " : "rounded-xl",
|
||||
" relative p-4",
|
||||
!isActive ? 'opacity-75 hover:opacity-100 ' : 'rounded-xl',
|
||||
' relative p-4',
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex size-9 items-center justify-center rounded-lg",
|
||||
isActive ? "bg-border" : "bg-muted",
|
||||
'flex size-9 items-center justify-center rounded-lg',
|
||||
isActive ? 'bg-border' : 'bg-muted',
|
||||
)}
|
||||
>
|
||||
<feature.icon />
|
||||
@@ -272,7 +294,7 @@ function Feature({
|
||||
layoutId="bubble"
|
||||
className="absolute inset-0 z-10 rounded-xl bg-white/5 mix-blend-difference"
|
||||
transition={{
|
||||
type: "spring",
|
||||
type: 'spring',
|
||||
bounce: 0.2,
|
||||
duration: 0.6,
|
||||
}}
|
||||
@@ -280,8 +302,8 @@ function Feature({
|
||||
)}
|
||||
<h3
|
||||
className={cn(
|
||||
"mt-6 text-sm font-medium",
|
||||
isActive ? "text-primary" : "text-primary/85",
|
||||
'mt-6 text-sm font-medium',
|
||||
isActive ? 'text-primary' : 'text-primary/85',
|
||||
)}
|
||||
>
|
||||
{feature.name}
|
||||
@@ -293,7 +315,7 @@ function Feature({
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function FeaturesMobile() {
|
||||
@@ -301,7 +323,11 @@ function FeaturesMobile() {
|
||||
<div className="-mx-4 mt-20 flex flex-col gap-y-10 overflow-hidden px-4 sm:-mx-6 sm:px-6 lg:hidden">
|
||||
{features.map((feature) => (
|
||||
<div key={feature.summary}>
|
||||
<Feature feature={feature} className="mx-auto max-w-2xl" isActive />
|
||||
<Feature
|
||||
feature={feature}
|
||||
className="mx-auto max-w-2xl"
|
||||
isActive
|
||||
/>
|
||||
<div className="relative mt-10 pb-10">
|
||||
<div className="absolute -inset-x-4 bottom-0 top-8 bg-muted sm:-inset-x-6" />
|
||||
<div className="relative mx-auto w-[52.75rem] overflow-hidden rounded-xl bg-white shadow-lg shadow-slate-900/5 ring-1 ring-slate-500/10">
|
||||
@@ -316,7 +342,7 @@ function FeaturesMobile() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function FeaturesDesktop() {
|
||||
@@ -349,8 +375,9 @@ function FeaturesDesktop() {
|
||||
static
|
||||
key={feature.summary}
|
||||
className={cn(
|
||||
"px-5 transition duration-500 ease-in-out ui-not-focus-visible:outline-none",
|
||||
featureIndex !== selectedIndex && "opacity-60",
|
||||
'px-5 transition duration-500 ease-in-out ui-not-focus-visible:outline-none',
|
||||
featureIndex !== selectedIndex &&
|
||||
'opacity-60',
|
||||
)}
|
||||
style={{
|
||||
transform: `translateX(-${selectedIndex * 100}%)`,
|
||||
@@ -373,7 +400,7 @@ function FeaturesDesktop() {
|
||||
</>
|
||||
)}
|
||||
</Tab.Group>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export function SecondaryFeatures() {
|
||||
@@ -389,12 +416,15 @@ export function SecondaryFeatures() {
|
||||
Advanced Management Tools
|
||||
</h2>
|
||||
<p className="mt-4 text-lg tracking-tight text-muted-foreground">
|
||||
Elevate your infrastructure with tools that offer precise control, detailed monitoring, and enhanced security, ensuring seamless management and robust performance.
|
||||
Elevate your infrastructure with tools that offer
|
||||
precise control, detailed monitoring, and enhanced
|
||||
security, ensuring seamless management and robust
|
||||
performance.
|
||||
</p>
|
||||
</div>
|
||||
<FeaturesMobile />
|
||||
<FeaturesDesktop />
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import Link from "next/link";
|
||||
import Link from 'next/link'
|
||||
|
||||
export function SlimLayout() {
|
||||
return (
|
||||
<>
|
||||
<main className="flex flex-auto items-center justify-center text-center">
|
||||
<div>
|
||||
<h1 className="mb-4 text-6xl font-semibold text-primary">404</h1>
|
||||
<p className="mb-4 text-lg text-muted-foreground">Not found.</p>
|
||||
<h1 className="mb-4 text-6xl font-semibold text-primary">
|
||||
404
|
||||
</h1>
|
||||
<p className="mb-4 text-lg text-muted-foreground">
|
||||
Not found.
|
||||
</p>
|
||||
<p className="mt-4 text-muted-foreground">
|
||||
Go back to home
|
||||
<Link href="/" className="text-primary">
|
||||
@@ -17,5 +21,5 @@ export function SlimLayout() {
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Marquee } from "./ui/marquee";
|
||||
'use client'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Marquee } from './ui/marquee'
|
||||
|
||||
// const testimonials = [
|
||||
// [
|
||||
@@ -75,75 +75,75 @@ import { Marquee } from "./ui/marquee";
|
||||
|
||||
const reviews = [
|
||||
{
|
||||
name: "Duras",
|
||||
username: "@duras",
|
||||
body: "This app convinced me to try something beyond pure Docker Compose. It’s a pleasure to contribute to such an awesome project!",
|
||||
img: "https://avatar.vercel.sh/duras",
|
||||
name: 'Duras',
|
||||
username: '@duras',
|
||||
body: 'This app convinced me to try something beyond pure Docker Compose. It’s a pleasure to contribute to such an awesome project!',
|
||||
img: 'https://avatar.vercel.sh/duras',
|
||||
},
|
||||
{
|
||||
name: "apis",
|
||||
username: "@apis",
|
||||
body: "I replaced my previous setup with Dokploy today. It’s stable, easy to use, and offers excellent support!",
|
||||
img: "https://avatar.vercel.sh/apis",
|
||||
name: 'apis',
|
||||
username: '@apis',
|
||||
body: 'I replaced my previous setup with Dokploy today. It’s stable, easy to use, and offers excellent support!',
|
||||
img: 'https://avatar.vercel.sh/apis',
|
||||
},
|
||||
{
|
||||
name: "yayza_",
|
||||
username: "@yayza_",
|
||||
body: "Migrated all my services to Dokploy—it worked seamlessly! The level of configuration is perfect for all kinds of projects.",
|
||||
img: "https://avatar.vercel.sh/yayza",
|
||||
name: 'yayza_',
|
||||
username: '@yayza_',
|
||||
body: 'Migrated all my services to Dokploy—it worked seamlessly! The level of configuration is perfect for all kinds of projects.',
|
||||
img: 'https://avatar.vercel.sh/yayza',
|
||||
},
|
||||
{
|
||||
name: "Vaurion",
|
||||
username: "@vaurion",
|
||||
body: "Dokploy makes my deployments incredibly easy. I just test locally, push a preview to GitHub, and Dokploy takes care of the rest.",
|
||||
img: "https://avatar.vercel.sh/vaurion",
|
||||
name: 'Vaurion',
|
||||
username: '@vaurion',
|
||||
body: 'Dokploy makes my deployments incredibly easy. I just test locally, push a preview to GitHub, and Dokploy takes care of the rest.',
|
||||
img: 'https://avatar.vercel.sh/vaurion',
|
||||
},
|
||||
{
|
||||
name: "vinum?",
|
||||
username: "@vinum",
|
||||
name: 'vinum?',
|
||||
username: '@vinum',
|
||||
body: "Dokploy is everything I wanted in a PaaS. The functionality is impressive, and it's completely free!",
|
||||
img: "https://avatar.vercel.sh/vinum",
|
||||
img: 'https://avatar.vercel.sh/vinum',
|
||||
},
|
||||
{
|
||||
name: "vadzim",
|
||||
username: "@vadzim",
|
||||
body: "Dokploy is fantastic! I rarely encounter any deployment issues, and the community support is top-notch.",
|
||||
img: "https://avatar.vercel.sh/vadzim",
|
||||
name: 'vadzim',
|
||||
username: '@vadzim',
|
||||
body: 'Dokploy is fantastic! I rarely encounter any deployment issues, and the community support is top-notch.',
|
||||
img: 'https://avatar.vercel.sh/vadzim',
|
||||
},
|
||||
{
|
||||
name: "Slurpy Beckerman",
|
||||
username: "@slurpy",
|
||||
body: "This is exactly what I want in a deployment system. I’ve restructured my dev process around Dokploy!",
|
||||
img: "https://avatar.vercel.sh/slurpy",
|
||||
name: 'Slurpy Beckerman',
|
||||
username: '@slurpy',
|
||||
body: 'This is exactly what I want in a deployment system. I’ve restructured my dev process around Dokploy!',
|
||||
img: 'https://avatar.vercel.sh/slurpy',
|
||||
},
|
||||
{
|
||||
name: "lua",
|
||||
username: "@lua",
|
||||
body: "Dokploy is genuinely so nice to use. The hard work behind it really shows.",
|
||||
img: "https://avatar.vercel.sh/lua",
|
||||
name: 'lua',
|
||||
username: '@lua',
|
||||
body: 'Dokploy is genuinely so nice to use. The hard work behind it really shows.',
|
||||
img: 'https://avatar.vercel.sh/lua',
|
||||
},
|
||||
{
|
||||
name: "johnnygri",
|
||||
username: "@johnnygri",
|
||||
body: "Dokploy is a complete joy to use. I’m running a mix of critical and low-priority services seamlessly across servers.",
|
||||
img: "https://avatar.vercel.sh/johnnygri",
|
||||
name: 'johnnygri',
|
||||
username: '@johnnygri',
|
||||
body: 'Dokploy is a complete joy to use. I’m running a mix of critical and low-priority services seamlessly across servers.',
|
||||
img: 'https://avatar.vercel.sh/johnnygri',
|
||||
},
|
||||
{
|
||||
name: "HiJoe",
|
||||
username: "@hijoe",
|
||||
body: "Setting up Dokploy was great—simple, intuitive, and reliable. Perfect for small to medium-sized businesses.",
|
||||
img: "https://avatar.vercel.sh/hijoe",
|
||||
name: 'HiJoe',
|
||||
username: '@hijoe',
|
||||
body: 'Setting up Dokploy was great—simple, intuitive, and reliable. Perfect for small to medium-sized businesses.',
|
||||
img: 'https://avatar.vercel.sh/hijoe',
|
||||
},
|
||||
{
|
||||
name: "johannes0910",
|
||||
username: "@johannes0910",
|
||||
body: "Dokploy has been a game-changer for my side projects. Solid UI, straightforward Docker abstraction, and great design.",
|
||||
img: "https://avatar.vercel.sh/johannes0910",
|
||||
name: 'johannes0910',
|
||||
username: '@johannes0910',
|
||||
body: 'Dokploy has been a game-changer for my side projects. Solid UI, straightforward Docker abstraction, and great design.',
|
||||
img: 'https://avatar.vercel.sh/johannes0910',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const firstRow = reviews.slice(0, reviews.length / 2);
|
||||
const secondRow = reviews.slice(reviews.length / 2);
|
||||
const firstRow = reviews.slice(0, reviews.length / 2)
|
||||
const secondRow = reviews.slice(reviews.length / 2)
|
||||
|
||||
const ReviewCard = ({
|
||||
img,
|
||||
@@ -151,34 +151,42 @@ const ReviewCard = ({
|
||||
username,
|
||||
body,
|
||||
}: {
|
||||
img: string;
|
||||
name: string;
|
||||
username: string;
|
||||
body: string;
|
||||
img: string
|
||||
name: string
|
||||
username: string
|
||||
body: string
|
||||
}) => {
|
||||
return (
|
||||
<figure
|
||||
className={cn(
|
||||
"relative w-64 cursor-pointer overflow-hidden rounded-xl border p-4",
|
||||
'relative w-64 cursor-pointer overflow-hidden rounded-xl border p-4',
|
||||
// light styles
|
||||
// "border-gray-950/[.1] bg-gray-950/[.01] hover:bg-gray-950/[.05]",
|
||||
// dark styles
|
||||
"hover:bg-gray-50/[.15]",
|
||||
'hover:bg-gray-50/[.15]',
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<img className="rounded-full" width="32" height="32" alt="" src={img} />
|
||||
<img
|
||||
className="rounded-full"
|
||||
width="32"
|
||||
height="32"
|
||||
alt=""
|
||||
src={img}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<figcaption className="text-sm font-medium text-white">
|
||||
{name}
|
||||
</figcaption>
|
||||
<p className="text-xs font-medium text-white/40">{username}</p>
|
||||
<p className="text-xs font-medium text-white/40">
|
||||
{username}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<blockquote className="mt-2 text-sm">{body}</blockquote>
|
||||
</figure>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export function Testimonials() {
|
||||
return (
|
||||
@@ -187,13 +195,14 @@ export function Testimonials() {
|
||||
aria-label="What our customers are saying"
|
||||
className=" py-20 sm:py-32"
|
||||
>
|
||||
<div className="mx-auto max-w-2xl md:text-center px-4">
|
||||
<h2 className="font-display text-3xl tracking-tight sm:text-4xl text-center">
|
||||
<div className="mx-auto max-w-2xl px-4 md:text-center">
|
||||
<h2 className="text-center font-display text-3xl tracking-tight sm:text-4xl">
|
||||
Why Developers Love Dokploy
|
||||
</h2>
|
||||
<p className="mt-4 text-lg tracking-tight text-muted-foreground text-center">
|
||||
Think we’re bragging? Hear from the devs who once doubted too—until
|
||||
Dokploy made their lives (and deployments) surprisingly easier.
|
||||
<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 Dokploy made their lives (and deployments)
|
||||
surprisingly easier.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -212,5 +221,5 @@ export function Testimonials() {
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 w-1/3 bg-gradient-to-l from-background" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { useEffect } from "react";
|
||||
import initializeGA from ".";
|
||||
import { useEffect } from 'react'
|
||||
import initializeGA from '.'
|
||||
|
||||
export default function GoogleAnalytics() {
|
||||
useEffect(() => {
|
||||
// @ts-ignore
|
||||
if (!window.GA_INITIALIZED) {
|
||||
initializeGA();
|
||||
initializeGA()
|
||||
// @ts-ignore
|
||||
window.GA_INITIALIZED = true;
|
||||
window.GA_INITIALIZED = true
|
||||
}
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import ReactGA from "react-ga4";
|
||||
import ReactGA from 'react-ga4'
|
||||
|
||||
const initializeGA = () => {
|
||||
// Replace with your Measurement ID
|
||||
// It ideally comes from an environment variable
|
||||
ReactGA.initialize("G-0RTZ5EPB26");
|
||||
ReactGA.initialize('G-0RTZ5EPB26')
|
||||
|
||||
// Don't forget to remove the console.log() statements
|
||||
// when you are done
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
category: string;
|
||||
action: string;
|
||||
label: string;
|
||||
category: string
|
||||
action: string
|
||||
label: string
|
||||
}
|
||||
const trackGAEvent = ({ category, action, label }: Props) => {
|
||||
console.log("GA event:", category, ":", action, ":", label);
|
||||
console.log('GA event:', category, ':', action, ':', label)
|
||||
// Send GA4 Event
|
||||
ReactGA.event({
|
||||
category: category,
|
||||
action: action,
|
||||
label: label,
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
export default initializeGA;
|
||||
export { initializeGA, trackGAEvent };
|
||||
export default initializeGA
|
||||
export { initializeGA, trackGAEvent }
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { Post } from "@/lib/ghost";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import type { Post } from '@/lib/ghost'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface BlogCardProps {
|
||||
post: Post;
|
||||
post: Post
|
||||
}
|
||||
|
||||
export function BlogCard({ post }: BlogCardProps) {
|
||||
const formattedDate = new Date(post.published_at).toLocaleDateString("en", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
const formattedDate = new Date(post.published_at).toLocaleDateString('en', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex flex-col overflow-hidden rounded-lg shadow-lg transition-all hover:shadow-xl">
|
||||
@@ -40,7 +40,9 @@ export function BlogCard({ post }: BlogCardProps) {
|
||||
<h3 className="text-xl font-semibold text-gray-900">
|
||||
{post.title}
|
||||
</h3>
|
||||
<p className="mt-3 text-base text-gray-500">{post.excerpt}</p>
|
||||
<p className="mt-3 text-base text-gray-500">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-6 flex items-center">
|
||||
@@ -56,10 +58,12 @@ export function BlogCard({ post }: BlogCardProps) {
|
||||
)}
|
||||
<div className="ml-3">
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{post.primary_author?.name || "Anonymous"}
|
||||
{post.primary_author?.name || 'Anonymous'}
|
||||
</p>
|
||||
<div className="flex space-x-1 text-sm text-gray-500">
|
||||
<time dateTime={post.published_at}>{formattedDate}</time>
|
||||
<time dateTime={post.published_at}>
|
||||
{formattedDate}
|
||||
</time>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span>{post.reading_time} min read</span>
|
||||
</div>
|
||||
@@ -67,5 +71,5 @@ export function BlogCard({ post }: BlogCardProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,161 +1,170 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { IconBrandYoutubeFilled } from "@tabler/icons-react";
|
||||
import { motion } from "framer-motion";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import type React from "react";
|
||||
import { cn } from '@/lib/utils'
|
||||
import { IconBrandYoutubeFilled } from '@tabler/icons-react'
|
||||
import { motion } from 'framer-motion'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import type React from 'react'
|
||||
|
||||
export function FeaturesSectionDemo() {
|
||||
const features = [
|
||||
{
|
||||
title: "Track issues effectively",
|
||||
title: 'Track issues effectively',
|
||||
description:
|
||||
"Track and manage your project issues with ease using our intuitive interface.",
|
||||
'Track and manage your project issues with ease using our intuitive interface.',
|
||||
skeleton: <SkeletonOne />,
|
||||
className:
|
||||
"col-span-1 lg:col-span-4 border-b lg:border-r dark:border-neutral-800",
|
||||
'col-span-1 lg:col-span-4 border-b lg:border-r dark:border-neutral-800',
|
||||
},
|
||||
{
|
||||
title: "Capture pictures with AI",
|
||||
title: 'Capture pictures with AI',
|
||||
description:
|
||||
"Capture stunning photos effortlessly using our advanced AI technology.",
|
||||
'Capture stunning photos effortlessly using our advanced AI technology.',
|
||||
skeleton: <SkeletonTwo />,
|
||||
className: "border-b col-span-1 lg:col-span-2 dark:border-neutral-800",
|
||||
className:
|
||||
'border-b col-span-1 lg:col-span-2 dark:border-neutral-800',
|
||||
},
|
||||
{
|
||||
title: "Watch our AI on YouTube",
|
||||
title: 'Watch our AI on YouTube',
|
||||
description:
|
||||
"Whether its you or Tyler Durden, you can get to know about our product on YouTube",
|
||||
'Whether its you or Tyler Durden, you can get to know about our product on YouTube',
|
||||
skeleton: <SkeletonThree />,
|
||||
className:
|
||||
"col-span-1 lg:col-span-3 lg:border-r dark:border-neutral-800",
|
||||
'col-span-1 lg:col-span-3 lg:border-r dark:border-neutral-800',
|
||||
},
|
||||
{
|
||||
title: "Deploy in seconds",
|
||||
title: 'Deploy in seconds',
|
||||
description:
|
||||
"With our blazing fast, state of the art, cutting edge, we are so back cloud servies (read AWS) - you can deploy your model in seconds.",
|
||||
'With our blazing fast, state of the art, cutting edge, we are so back cloud servies (read AWS) - you can deploy your model in seconds.',
|
||||
skeleton: <SkeletonFour />,
|
||||
className: "col-span-1 lg:col-span-3 border-b lg:border-none",
|
||||
className: 'col-span-1 lg:col-span-3 border-b lg:border-none',
|
||||
},
|
||||
];
|
||||
]
|
||||
return (
|
||||
<div className="relative z-20 py-10 lg:py-40 max-w-7xl mx-auto">
|
||||
<div className="relative z-20 mx-auto max-w-7xl py-10 lg:py-40">
|
||||
<div className="px-8">
|
||||
<h4 className="text-3xl lg:text-5xl lg:leading-tight max-w-5xl mx-auto text-center tracking-tight font-medium text-black dark:text-white">
|
||||
<h4 className="mx-auto max-w-5xl text-center text-3xl font-medium tracking-tight text-black dark:text-white lg:text-5xl lg:leading-tight">
|
||||
Packed with thousands of features
|
||||
</h4>
|
||||
|
||||
<p className="text-sm lg:text-base max-w-2xl my-4 mx-auto text-neutral-500 text-center font-normal dark:text-neutral-300">
|
||||
From Image generation to video generation, Everything AI has APIs for
|
||||
literally everything. It can even create this website copy for you.
|
||||
<p className="mx-auto my-4 max-w-2xl text-center text-sm font-normal text-neutral-500 dark:text-neutral-300 lg:text-base">
|
||||
From Image generation to video generation, Everything AI has
|
||||
APIs for literally everything. It can even create this
|
||||
website copy for you.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative ">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-6 mt-12 xl:border rounded-md dark:border-neutral-800">
|
||||
<div className="mt-12 grid grid-cols-1 rounded-md dark:border-neutral-800 lg:grid-cols-6 xl:border">
|
||||
{features.map((feature) => (
|
||||
<FeatureCard key={feature.title} className={feature.className}>
|
||||
<FeatureCard
|
||||
key={feature.title}
|
||||
className={feature.className}
|
||||
>
|
||||
<FeatureTitle>{feature.title}</FeatureTitle>
|
||||
<FeatureDescription>{feature.description}</FeatureDescription>
|
||||
<div className=" h-full w-full">{feature.skeleton}</div>
|
||||
<FeatureDescription>
|
||||
{feature.description}
|
||||
</FeatureDescription>
|
||||
<div className=" h-full w-full">
|
||||
{feature.skeleton}
|
||||
</div>
|
||||
</FeatureCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const FeatureCard = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn("p-4 sm:p-8 relative overflow-hidden", className)}>
|
||||
<div className={cn('relative overflow-hidden p-4 sm:p-8', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const FeatureTitle = ({ children }: { children?: React.ReactNode }) => {
|
||||
return (
|
||||
<p className=" max-w-5xl mx-auto text-left tracking-tight text-black dark:text-white text-xl md:text-2xl md:leading-snug">
|
||||
<p className=" mx-auto max-w-5xl text-left text-xl tracking-tight text-black dark:text-white md:text-2xl md:leading-snug">
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const FeatureDescription = ({ children }: { children?: React.ReactNode }) => {
|
||||
return (
|
||||
<p
|
||||
className={cn(
|
||||
"text-sm md:text-base max-w-4xl text-left mx-auto",
|
||||
"text-neutral-500 text-center font-normal dark:text-neutral-300",
|
||||
"text-left max-w-sm mx-0 md:text-sm my-2",
|
||||
'mx-auto max-w-4xl text-left text-sm md:text-base',
|
||||
'text-center font-normal text-neutral-500 dark:text-neutral-300',
|
||||
'mx-0 my-2 max-w-sm text-left md:text-sm',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export const SkeletonOne = () => {
|
||||
return (
|
||||
<div className="relative flex py-8 px-2 gap-10 h-full">
|
||||
<div className="w-full p-5 mx-auto bg-white dark:bg-neutral-900 shadow-2xl group h-full">
|
||||
<div className="flex flex-1 w-full h-full flex-col space-y-2 ">
|
||||
<div className="relative flex h-full gap-10 px-2 py-8">
|
||||
<div className="group mx-auto h-full w-full bg-white p-5 shadow-2xl dark:bg-neutral-900">
|
||||
<div className="flex h-full w-full flex-1 flex-col space-y-2 ">
|
||||
{/* TODO */}
|
||||
<Image
|
||||
src="/linear.webp"
|
||||
alt="header"
|
||||
width={800}
|
||||
height={800}
|
||||
className="h-full w-full aspect-square object-cover object-left-top rounded-sm"
|
||||
className="aspect-square h-full w-full rounded-sm object-cover object-left-top"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-0 z-40 inset-x-0 h-60 bg-gradient-to-t from-white dark:from-black via-white dark:via-black to-transparent w-full pointer-events-none" />
|
||||
<div className="absolute top-0 z-40 inset-x-0 h-60 bg-gradient-to-b from-white dark:from-black via-transparent to-transparent w-full pointer-events-none" />
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-40 h-60 w-full bg-gradient-to-t from-white via-white to-transparent dark:from-black dark:via-black" />
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 z-40 h-60 w-full bg-gradient-to-b from-white via-transparent to-transparent dark:from-black" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export const SkeletonThree = () => {
|
||||
return (
|
||||
<Link
|
||||
href="https://www.youtube.com/watch?v=RPa3_AD1_Vs"
|
||||
target="__blank"
|
||||
className="relative flex gap-10 h-full group/image"
|
||||
className="group/image relative flex h-full gap-10"
|
||||
>
|
||||
<div className="w-full mx-auto bg-transparent dark:bg-transparent group h-full">
|
||||
<div className="flex flex-1 w-full h-full flex-col space-y-2 relative">
|
||||
<div className="group mx-auto h-full w-full bg-transparent dark:bg-transparent">
|
||||
<div className="relative flex h-full w-full flex-1 flex-col space-y-2">
|
||||
{/* TODO */}
|
||||
<IconBrandYoutubeFilled className="h-20 w-20 absolute z-10 inset-0 text-red-500 m-auto " />
|
||||
<IconBrandYoutubeFilled className="absolute inset-0 z-10 m-auto h-20 w-20 text-red-500 " />
|
||||
<Image
|
||||
src="https://assets.aceternity.com/fireship.jpg"
|
||||
alt="header"
|
||||
width={800}
|
||||
height={800}
|
||||
className="h-full w-full aspect-square object-cover object-center rounded-sm blur-none group-hover/image:blur-md transition-all duration-200"
|
||||
className="aspect-square h-full w-full rounded-sm object-cover object-center blur-none transition-all duration-200 group-hover/image:blur-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export const SkeletonTwo = () => {
|
||||
const images = [
|
||||
"https://images.unsplash.com/photo-1517322048670-4fba75cbbb62?q=80&w=3000&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
"https://images.unsplash.com/photo-1573790387438-4da905039392?q=80&w=3425&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
"https://images.unsplash.com/photo-1555400038-63f5ba517a47?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
"https://images.unsplash.com/photo-1554931670-4ebfabf6e7a9?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
"https://images.unsplash.com/photo-1546484475-7f7bd55792da?q=80&w=2581&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
];
|
||||
'https://images.unsplash.com/photo-1517322048670-4fba75cbbb62?q=80&w=3000&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
||||
'https://images.unsplash.com/photo-1573790387438-4da905039392?q=80&w=3425&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
||||
'https://images.unsplash.com/photo-1555400038-63f5ba517a47?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
||||
'https://images.unsplash.com/photo-1554931670-4ebfabf6e7a9?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
||||
'https://images.unsplash.com/photo-1546484475-7f7bd55792da?q=80&w=2581&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
||||
]
|
||||
|
||||
const imageVariants = {
|
||||
whileHover: {
|
||||
@@ -168,11 +177,11 @@ export const SkeletonTwo = () => {
|
||||
rotate: 0,
|
||||
zIndex: 100,
|
||||
},
|
||||
};
|
||||
}
|
||||
return (
|
||||
<div className="relative flex flex-col items-start p-8 gap-10 h-full overflow-hidden">
|
||||
<div className="relative flex h-full flex-col items-start gap-10 overflow-hidden p-8">
|
||||
{/* TODO */}
|
||||
<div className="flex flex-row -ml-20">
|
||||
<div className="-ml-20 flex flex-row">
|
||||
{images.map((image, idx) => (
|
||||
<motion.div
|
||||
variants={imageVariants}
|
||||
@@ -182,14 +191,14 @@ export const SkeletonTwo = () => {
|
||||
}}
|
||||
whileHover="whileHover"
|
||||
whileTap="whileTap"
|
||||
className="rounded-xl -mr-4 mt-4 p-1 bg-white dark:bg-neutral-800 dark:border-neutral-700 border border-neutral-100 flex-shrink-0 overflow-hidden"
|
||||
className="-mr-4 mt-4 flex-shrink-0 overflow-hidden rounded-xl border border-neutral-100 bg-white p-1 dark:border-neutral-700 dark:bg-neutral-800"
|
||||
>
|
||||
<Image
|
||||
src={image}
|
||||
alt="bali images"
|
||||
width="500"
|
||||
height="500"
|
||||
className="rounded-lg h-20 w-20 md:h-40 md:w-40 object-cover flex-shrink-0"
|
||||
className="h-20 w-20 flex-shrink-0 rounded-lg object-cover md:h-40 md:w-40"
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
@@ -204,29 +213,29 @@ export const SkeletonTwo = () => {
|
||||
variants={imageVariants}
|
||||
whileHover="whileHover"
|
||||
whileTap="whileTap"
|
||||
className="rounded-xl -mr-4 mt-4 p-1 bg-white dark:bg-neutral-800 dark:border-neutral-700 border border-neutral-100 flex-shrink-0 overflow-hidden"
|
||||
className="-mr-4 mt-4 flex-shrink-0 overflow-hidden rounded-xl border border-neutral-100 bg-white p-1 dark:border-neutral-700 dark:bg-neutral-800"
|
||||
>
|
||||
<Image
|
||||
src={image}
|
||||
alt="bali images"
|
||||
width="500"
|
||||
height="500"
|
||||
className="rounded-lg h-20 w-20 md:h-40 md:w-40 object-cover flex-shrink-0"
|
||||
className="h-20 w-20 flex-shrink-0 rounded-lg object-cover md:h-40 md:w-40"
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="absolute left-0 z-[100] inset-y-0 w-20 bg-gradient-to-r from-white dark:from-black to-transparent h-full pointer-events-none" />
|
||||
<div className="absolute right-0 z-[100] inset-y-0 w-20 bg-gradient-to-l from-white dark:from-black to-transparent h-full pointer-events-none" />
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 z-[100] h-full w-20 bg-gradient-to-r from-white to-transparent dark:from-black" />
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 z-[100] h-full w-20 bg-gradient-to-l from-white to-transparent dark:from-black" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export const SkeletonFour = () => {
|
||||
return (
|
||||
<div className="h-60 md:h-60 flex flex-col items-center relative bg-transparent dark:bg-transparent mt-10">
|
||||
<div className="relative mt-10 flex h-60 flex-col items-center bg-transparent dark:bg-transparent md:h-60">
|
||||
{/* <Globe className="absolute -right-10 md:-right-10 -bottom-80 md:-bottom-72" /> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
'use client'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
IconActivity,
|
||||
IconCloud,
|
||||
@@ -10,101 +10,101 @@ import {
|
||||
IconTerminal,
|
||||
IconTerminal2,
|
||||
IconUsers,
|
||||
} from "@tabler/icons-react";
|
||||
import { Layers, Lock, UnlockIcon } from "lucide-react";
|
||||
} from '@tabler/icons-react'
|
||||
import { Layers, Lock, UnlockIcon } from 'lucide-react'
|
||||
|
||||
export function FirstFeaturesSection() {
|
||||
const features = [
|
||||
{
|
||||
title: "Flexible Application Deployment",
|
||||
title: 'Flexible Application Deployment',
|
||||
description:
|
||||
"Deploy any application using Nixpacks, Heroku Buildpacks, or your custom Dockerfile, tailored to your stack.",
|
||||
'Deploy any application using Nixpacks, Heroku Buildpacks, or your custom Dockerfile, tailored to your stack.',
|
||||
icon: <IconRocket />,
|
||||
},
|
||||
{
|
||||
title: "Native Docker Compose Support",
|
||||
title: 'Native Docker Compose Support',
|
||||
description:
|
||||
"Deploy complex applications natively with full Docker Compose integration for seamless orchestration.",
|
||||
'Deploy complex applications natively with full Docker Compose integration for seamless orchestration.',
|
||||
icon: <Layers />,
|
||||
},
|
||||
{
|
||||
title: "Multi-server Support",
|
||||
title: 'Multi-server Support',
|
||||
description:
|
||||
"Effortlessly deploy your applications on remote servers, with zero configuration hassle.",
|
||||
'Effortlessly deploy your applications on remote servers, with zero configuration hassle.',
|
||||
icon: <IconCloud />,
|
||||
},
|
||||
{
|
||||
title: "Advanced User Management",
|
||||
title: 'Advanced User Management',
|
||||
description:
|
||||
"Control user access with detailed roles and permissions, keeping your deployments secure and organized.",
|
||||
'Control user access with detailed roles and permissions, keeping your deployments secure and organized.',
|
||||
icon: <IconUsers />,
|
||||
},
|
||||
{
|
||||
title: "Database Management with Backups",
|
||||
title: 'Database Management with Backups',
|
||||
description:
|
||||
"Manage and back up MySQL, PostgreSQL, MongoDB, MariaDB, Redis directly from Dokploy.",
|
||||
'Manage and back up MySQL, PostgreSQL, MongoDB, MariaDB, Redis directly from Dokploy.',
|
||||
icon: <IconDatabase />,
|
||||
},
|
||||
{
|
||||
title: "API & CLI Access",
|
||||
title: 'API & CLI Access',
|
||||
description:
|
||||
"Need custom functionality? Dokploy offers complete API and CLI access to fit your needs.",
|
||||
'Need custom functionality? Dokploy offers complete API and CLI access to fit your needs.',
|
||||
icon: <IconTerminal />,
|
||||
},
|
||||
{
|
||||
title: "Docker Swarm Clusters",
|
||||
title: 'Docker Swarm Clusters',
|
||||
description:
|
||||
"Scale your deployments seamlessly with built-in Docker Swarm support for robust, multi-node applications.",
|
||||
'Scale your deployments seamlessly with built-in Docker Swarm support for robust, multi-node applications.',
|
||||
icon: <IconUsers />,
|
||||
},
|
||||
{
|
||||
title: "Open Source Templates",
|
||||
title: 'Open Source Templates',
|
||||
description:
|
||||
"Get started quickly with pre-configured templates for popular tools like Supabase, Cal.com, and PocketBase.",
|
||||
'Get started quickly with pre-configured templates for popular tools like Supabase, Cal.com, and PocketBase.',
|
||||
icon: <IconTemplate />,
|
||||
},
|
||||
{
|
||||
title: "No Vendor Lock-In",
|
||||
title: 'No Vendor Lock-In',
|
||||
description:
|
||||
"Experience complete freedom to modify, scale, and customize Dokploy to suit your specific needs.",
|
||||
'Experience complete freedom to modify, scale, and customize Dokploy to suit your specific needs.',
|
||||
icon: <UnlockIcon />,
|
||||
},
|
||||
{
|
||||
title: "Real-time Monitoring & Alerts",
|
||||
title: 'Real-time Monitoring & Alerts',
|
||||
description:
|
||||
"Monitor CPU, memory, and network usage in real-time across your deployments for full visibility.",
|
||||
'Monitor CPU, memory, and network usage in real-time across your deployments for full visibility.',
|
||||
icon: <IconActivity />,
|
||||
},
|
||||
{
|
||||
title: "Built for Developers",
|
||||
title: 'Built for Developers',
|
||||
description:
|
||||
"Designed specifically for engineers and developers seeking control and flexibility.",
|
||||
'Designed specifically for engineers and developers seeking control and flexibility.',
|
||||
icon: <IconTerminal2 />,
|
||||
},
|
||||
{
|
||||
title: "Self-hosted & Open Source",
|
||||
title: 'Self-hosted & Open Source',
|
||||
description:
|
||||
"Dokploy provides complete control with self-hosting capabilities and open-source transparency.",
|
||||
'Dokploy provides complete control with self-hosting capabilities and open-source transparency.',
|
||||
icon: <IconEaseInOut />,
|
||||
},
|
||||
];
|
||||
]
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center mt-20 px-4">
|
||||
<h2 className="font-display text-3xl tracking-tight text-primary sm:text-4xl text-center">
|
||||
<div className="mt-20 flex flex-col items-center justify-center px-4">
|
||||
<h2 className="text-center font-display text-3xl tracking-tight text-primary sm:text-4xl">
|
||||
Powerful Deployment Tailored to You
|
||||
</h2>
|
||||
<p className="mt-4 text-lg tracking-tight text-muted-foreground text-center">
|
||||
Unlock seamless multi-server deployments, advanced user control, and
|
||||
flexible database management—all with Dokploy’s developer-focused
|
||||
features.
|
||||
<p className="mt-4 text-center text-lg tracking-tight text-muted-foreground">
|
||||
Unlock seamless multi-server deployments, advanced user control,
|
||||
and flexible database management—all with Dokploy’s
|
||||
developer-focused features.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 relative z-10 py-10 max-w-7xl mx-auto mt-10 max-sm:p-0 max-sm:mx-0 max-sm:w-full">
|
||||
<div className="relative z-10 mx-auto mt-10 grid max-w-7xl grid-cols-1 py-10 max-sm:mx-0 max-sm:w-full max-sm:p-0 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{features.map((feature, index) => (
|
||||
<Feature key={feature.title} {...feature} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const Feature = ({
|
||||
@@ -113,36 +113,39 @@ const Feature = ({
|
||||
icon,
|
||||
index,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
index: number;
|
||||
title: string
|
||||
description: string
|
||||
icon: React.ReactNode
|
||||
index: number
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col lg:border-r py-10 relative group/feature border-neutral-800",
|
||||
'group/feature relative flex flex-col border-neutral-800 py-10 lg:border-r',
|
||||
(index === 0 || index === 4 || index === 8) &&
|
||||
"lg:border-l dark:border-neutral-800",
|
||||
(index < 4 || index < 8) && "lg:border-b dark:border-neutral-800",
|
||||
'dark:border-neutral-800 lg:border-l',
|
||||
(index < 4 || index < 8) &&
|
||||
'dark:border-neutral-800 lg:border-b',
|
||||
)}
|
||||
>
|
||||
{index < 4 && (
|
||||
<div className="opacity-0 group-hover/feature:opacity-100 transition duration-200 absolute inset-0 h-full w-full bg-gradient-to-t from-neutral-800 to-transparent pointer-events-none" />
|
||||
<div className="pointer-events-none absolute inset-0 h-full w-full bg-gradient-to-t from-neutral-800 to-transparent opacity-0 transition duration-200 group-hover/feature:opacity-100" />
|
||||
)}
|
||||
{index >= 4 && (
|
||||
<div className="opacity-0 group-hover/feature:opacity-100 transition duration-200 absolute inset-0 h-full w-full bg-gradient-to-b from-neutral-800 to-transparent pointer-events-none" />
|
||||
<div className="pointer-events-none absolute inset-0 h-full w-full bg-gradient-to-b from-neutral-800 to-transparent opacity-0 transition duration-200 group-hover/feature:opacity-100" />
|
||||
)}
|
||||
<div className="mb-4 relative z-10 px-10 text-neutral-400">{icon}</div>
|
||||
<div className="text-lg font-bold mb-2 relative z-10 px-10">
|
||||
<div className="absolute left-0 inset-y-0 h-6 group-hover/feature:h-8 w-1 rounded-tr-full rounded-br-full bg-neutral-700 group-hover/feature:bg-white transition-all duration-200 origin-center" />
|
||||
<span className="group-hover/feature:translate-x-2 transition duration-200 inline-block text-neutral-100">
|
||||
<div className="relative z-10 mb-4 px-10 text-neutral-400">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="relative z-10 mb-2 px-10 text-lg font-bold">
|
||||
<div className="absolute inset-y-0 left-0 h-6 w-1 origin-center rounded-br-full rounded-tr-full bg-neutral-700 transition-all duration-200 group-hover/feature:h-8 group-hover/feature:bg-white" />
|
||||
<span className="inline-block text-neutral-100 transition duration-200 group-hover/feature:translate-x-2">
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-neutral-300 lg:max-w-xs relative z-10 px-10">
|
||||
<p className="relative z-10 px-10 text-sm text-neutral-300 lg:max-w-xs">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const navigation = [
|
||||
{ name: "home", href: "/" },
|
||||
{ name: "features", href: "/features" },
|
||||
{ name: "pricing", href: "/pricing" },
|
||||
{ name: "blog", href: "/blog" },
|
||||
];
|
||||
{ name: 'home', href: '/' },
|
||||
{ name: 'features', href: '/features' },
|
||||
{ name: 'pricing', href: '/pricing' },
|
||||
{ name: 'blog', href: '/blog' },
|
||||
]
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,80 +1,87 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Tab } from '@headlessui/react'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: "Applications & Databases",
|
||||
description: "Centralize control of your applications and databases for enhanced security and efficiency, simplifying access and management across your infrastructure.",
|
||||
image: "/dashboard.png",
|
||||
title: 'Applications & Databases',
|
||||
description:
|
||||
'Centralize control of your applications and databases for enhanced security and efficiency, simplifying access and management across your infrastructure.',
|
||||
image: '/dashboard.png',
|
||||
},
|
||||
{
|
||||
title: "Docker Compose",
|
||||
description: "Native Docker Compose support so you can manage complex applications and services with ease.",
|
||||
image: "/compose.png",
|
||||
title: 'Docker Compose',
|
||||
description:
|
||||
'Native Docker Compose support so you can manage complex applications and services with ease.',
|
||||
image: '/compose.png',
|
||||
},
|
||||
{
|
||||
title: "Multiserver",
|
||||
description: "Deploy applications to multiple servers without the extra effort.",
|
||||
image: "/remote.png",
|
||||
title: 'Multiserver',
|
||||
description:
|
||||
'Deploy applications to multiple servers without the extra effort.',
|
||||
image: '/remote.png',
|
||||
},
|
||||
{
|
||||
title: "Logs",
|
||||
description: "Monitor and manage your applications' logs with ease, ensuring efficient troubleshooting and optimal performance.",
|
||||
image: "/logs.png",
|
||||
title: 'Logs',
|
||||
description:
|
||||
"Monitor and manage your applications' logs with ease, ensuring efficient troubleshooting and optimal performance.",
|
||||
image: '/logs.png',
|
||||
},
|
||||
{
|
||||
title: "Monitoring",
|
||||
description: "Monitor your systems' performance and health in real time, ensuring continuous and uninterrupted operation.",
|
||||
image: "/primary/monitoring.png",
|
||||
title: 'Monitoring',
|
||||
description:
|
||||
"Monitor your systems' performance and health in real time, ensuring continuous and uninterrupted operation.",
|
||||
image: '/primary/monitoring.png',
|
||||
},
|
||||
{
|
||||
title: "Backups",
|
||||
description: "Implement automatic and secure backup solutions to protect your critical data and restore it quickly when necessary.",
|
||||
image: "/backups.png",
|
||||
title: 'Backups',
|
||||
description:
|
||||
'Implement automatic and secure backup solutions to protect your critical data and restore it quickly when necessary.',
|
||||
image: '/backups.png',
|
||||
},
|
||||
{
|
||||
title: "Traefik",
|
||||
description: "Manage Traefik via File Editor to configure your own domain names, certificates, and more.",
|
||||
image: "/traefik.png",
|
||||
title: 'Traefik',
|
||||
description:
|
||||
'Manage Traefik via File Editor to configure your own domain names, certificates, and more.',
|
||||
image: '/traefik.png',
|
||||
},
|
||||
{
|
||||
title: "Templates",
|
||||
description: "Deploy open source templates with one click.",
|
||||
image: "/templates.png",
|
||||
title: 'Templates',
|
||||
description: 'Deploy open source templates with one click.',
|
||||
image: '/templates.png',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
export function SecondaryFeaturesSections() {
|
||||
const [tabOrientation, setTabOrientation] = useState<
|
||||
"horizontal" | "vertical"
|
||||
>("horizontal");
|
||||
'horizontal' | 'vertical'
|
||||
>('horizontal')
|
||||
|
||||
useEffect(() => {
|
||||
const lgMediaQuery = window.matchMedia("(min-width: 1024px)");
|
||||
const lgMediaQuery = window.matchMedia('(min-width: 1024px)')
|
||||
|
||||
function onMediaQueryChange({ matches }: { matches: boolean }) {
|
||||
setTabOrientation(matches ? "vertical" : "horizontal");
|
||||
setTabOrientation(matches ? 'vertical' : 'horizontal')
|
||||
}
|
||||
|
||||
onMediaQueryChange(lgMediaQuery);
|
||||
lgMediaQuery.addEventListener("change", onMediaQueryChange);
|
||||
onMediaQueryChange(lgMediaQuery)
|
||||
lgMediaQuery.addEventListener('change', onMediaQueryChange)
|
||||
|
||||
return () => {
|
||||
lgMediaQuery.removeEventListener("change", onMediaQueryChange);
|
||||
};
|
||||
}, []);
|
||||
lgMediaQuery.removeEventListener('change', onMediaQueryChange)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
|
||||
// Cambiar isMounted a true después del primer render
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
setIsMounted(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -82,13 +89,15 @@ export function SecondaryFeaturesSections() {
|
||||
aria-label="Features for running your books"
|
||||
className="relative overflow-hidden bg-black pb-28 pt-20 sm:py-32"
|
||||
>
|
||||
<div className="mx-auto max-w-7xl max-lg:px-4 relative">
|
||||
<div className="relative mx-auto max-w-7xl max-lg:px-4">
|
||||
<div className="max-w-2xl md:mx-auto md:text-center xl:max-w-none">
|
||||
<h2 className="font-display text-3xl tracking-tight text-white sm:text-4xl md:text-5xl">
|
||||
Comprehensive Control of Your Digital Ecosystem
|
||||
</h2>
|
||||
<p className="mt-6 text-lg tracking-tight text-muted-foreground">
|
||||
Simplify your project and data management, ensure robust monitoring, and secure your 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>
|
||||
</div>
|
||||
<Tab.Group
|
||||
@@ -98,7 +107,7 @@ export function SecondaryFeaturesSections() {
|
||||
>
|
||||
{({ selectedIndex }) => (
|
||||
<>
|
||||
<div className="-mx-4 flex overflow-x-auto pb-4 sm:mx-0 overflow-visible sm:pb-0">
|
||||
<div className="-mx-4 flex overflow-visible overflow-x-auto pb-4 sm:mx-0 sm:pb-0">
|
||||
<Tab.List
|
||||
aria-description="primary feature tabs"
|
||||
aria-roledescription="primary feature tabs"
|
||||
@@ -110,11 +119,12 @@ export function SecondaryFeaturesSections() {
|
||||
initial={false}
|
||||
key={`feature-${featureIndex}`}
|
||||
className={cn(
|
||||
"group relative rounded-full px-4 py-1 transition-colors ",
|
||||
'group relative rounded-full px-4 py-1 transition-colors ',
|
||||
)}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{selectedIndex === featureIndex && (
|
||||
{selectedIndex ===
|
||||
featureIndex && (
|
||||
<motion.span
|
||||
layoutId="tab"
|
||||
className="absolute inset-0 z-10 rounded-full bg-white/5 mix-blend-difference"
|
||||
@@ -122,7 +132,7 @@ export function SecondaryFeaturesSections() {
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
type: 'spring',
|
||||
bounce: 0.2,
|
||||
duration: 0.5,
|
||||
}}
|
||||
@@ -132,7 +142,7 @@ export function SecondaryFeaturesSections() {
|
||||
<h3>
|
||||
<Tab
|
||||
className={cn(
|
||||
"font-display text-lg text-primary ui-not-focus-visible:outline-none",
|
||||
'font-display text-lg text-primary ui-not-focus-visible:outline-none',
|
||||
)}
|
||||
>
|
||||
<span className="absolute inset-0 rounded-full" />
|
||||
@@ -141,7 +151,7 @@ export function SecondaryFeaturesSections() {
|
||||
</h3>
|
||||
<p
|
||||
className={cn(
|
||||
"mt-2 hidden text-sm text-muted-foreground ",
|
||||
'mt-2 hidden text-sm text-muted-foreground ',
|
||||
)}
|
||||
>
|
||||
{feature.description}
|
||||
@@ -155,18 +165,24 @@ export function SecondaryFeaturesSections() {
|
||||
<Tab.Panel key={`panel-${index}`}>
|
||||
<div className="relative sm:px-6 ">
|
||||
<div className="absolute -inset-x-4 bottom-[-4.25rem] top-[-6.5rem] bg-card/60 ring-1 ring-inset ring-white/10 sm:inset-x-0 sm:rounded-t-xl" />
|
||||
<p className="relative mx-auto max-w-2xl text-base text-white sm:text-center mb-10">
|
||||
<p className="relative mx-auto mb-10 max-w-2xl text-base text-white sm:text-center">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
key={feature.title}
|
||||
initial={isMounted ? { opacity: 0.4 } : {}}
|
||||
animate={isMounted ? { opacity: 1 } : {}}
|
||||
initial={
|
||||
isMounted
|
||||
? { opacity: 0.4 }
|
||||
: {}
|
||||
}
|
||||
animate={
|
||||
isMounted ? { opacity: 1 } : {}
|
||||
}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
type: 'spring',
|
||||
bounce: 0.2,
|
||||
duration: 0.8,
|
||||
}}
|
||||
@@ -174,13 +190,16 @@ export function SecondaryFeaturesSections() {
|
||||
>
|
||||
<div className="relative w-full">
|
||||
<div className="mx-auto">
|
||||
<div className="w-full h-11 rounded-t-lg bg-card flex justify-start items-center space-x-1.5 px-3">
|
||||
<span className="w-3 h-3 rounded-full bg-red-400" />
|
||||
<span className="w-3 h-3 rounded-full bg-yellow-400" />
|
||||
<span className="w-3 h-3 rounded-full bg-green-400" />
|
||||
<div className="flex h-11 w-full items-center justify-start space-x-1.5 rounded-t-lg bg-card px-3">
|
||||
<span className="h-3 w-3 rounded-full bg-red-400" />
|
||||
<span className="h-3 w-3 rounded-full bg-yellow-400" />
|
||||
<span className="h-3 w-3 rounded-full bg-green-400" />
|
||||
</div>
|
||||
<div className="bg-gray-100 w-full h-96">
|
||||
<img src={feature.image} alt={feature.title} />
|
||||
<div className="h-96 w-full bg-gray-100">
|
||||
<img
|
||||
src={feature.image}
|
||||
alt={feature.title}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,5 +212,5 @@ export function SecondaryFeaturesSections() {
|
||||
</Tab.Group>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export function Logo(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
export function Logo(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -21,5 +21,5 @@ export function Logo(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
d="M465 211h-1c-18.2 14.6-41.2 24.6-60 39-19 14.2-42.7 29.3-66 34l-4 1c-2.4 1-4 2-7 2h-1q-3.5 2-8 2h-1c-1.3 1.2-3 1.1-5 1h-2q-2.6 1.1-6 1h-2c-3 1.2-6.5 1-10 1-6.3.6-13.8.6-20 0-3.4 0-8.4.9-11-1h-1c-2.2.2-4.5.3-6-1h-1c-2 .2-3.7.2-5-1h-1c-7.6.5-16.5-3.4-23-6l-4-1a129 129 0 0 1-36.2-15.8c-10.4-6.6-23.2-12.8-32.5-20.5-9.2-7.7-23.8-12.8-30.3-22.7h-1c-2.3-1.4-4.5-2.7-6-5h-1c-4-2.5-8.5-5.2-12-8h-9a9 9 0 0 0-6 7c.3 3.3 0 6.7 0 10v9c.2 1.6 1 3.8 1 6v3c.2 1 1.2 2.2 1 4v1c1.2 1.2.8 2.2 1 4 .8 6.7 3 12.6 5 19 1.7 4.3 4.2 9.1 5 14v1q1.8 1.5 2 4v1a36 36 0 0 1 5 10c.7 2 1 3 2 5 8 12.7 15.7 25.5 25.8 37.3 10 11.7 20.8 20.6 32.4 30.4 11.7 9.9 28.3 14 39.8 23.3h1q2.5.3 4 2h1c2.8.4 4.8 2 7 3l7 2c5.7 1.3 13 2.3 18 5h1c2.1-.3 3.6.8 5 1h3c2.8.2 5.8 1 8 2h8c2.1 0 4.6.8 6 1h21c1.2-.2 3.2-1 5-1h9c3.3-1 7-2.4 11-2h1c2.7-2.2 7.4-2.4 11-3a55 55 0 0 0 8-2c6.5-2.6 13.9-6.3 21-8h1c8.5-6.8 20.6-9.7 29.2-16.8 8.7-7 18.3-12.8 26.8-20.2 4.4-3.8 9-9 13-13 14.8-14.8 20.7-34.6 33-50v-1q.9-3.4 3-6v-1q.3-2.5 2-4v-1c.5-3.3 2-8.6 4-11v-1q0-3.5 2-6v-1c1.1-6.7 2.4-15 5-21v-1c-.2-2-.2-3.7 1-5v-8c0-5.3-.5-10.8 0-16a14 14 0 0 0-4-6c-1-.5-1.1-.4-2-1h-6q-2.1 1.5-5 2m-6 38c-2.1 13.4-21.2 20.3-31 30-10 9.5-23.7 19-35 27-11.5 8-25.1 19.7-39 23h-1a22 22 0 0 1-10 4h-1a25 25 0 0 1-12 4h-1q-3.5 2-8 2h-1c-1.1 1.1-2.3 1-4 1h-2c-1.2.4-2.2 1-4 1h-2c-1.8.7-3.6 1.3-6 1h-1c-1.2 1.2-2.3 1-4 1h-5c-5.7.6-12.3.8-18 0h-4c-1.9 0-2.7-.6-4-1h-6c-1.9 0-2.7.3-4-1h-1q-2.5.5-4-1h-1c-8.1.5-16.8-3.6-24.2-5.8S210 329.8 204 325h-1c-12.8-5-27.1-15.6-37.7-24.3S138.8 284.2 131 273c-.3-.2-1 0-1 0-5.7-4.4-16.6-10-19-17-.9-2.6-1-5.4-2-8-.8-2.2-2.5-5-2-8a667 667 0 0 0 88 56h1q3.4.9 6 3h1c2.8.4 4.8 2 7 3q5 1.8 10 3l6 2q2.9.6 6 1 3 .4 5 1c1.6-.2 2 0 3 1h1c2-.2 3.7-.2 5 1h1c2.2-.3 3.4.4 5 1h8c1.6 0 3 .9 4 1h40c1.8-1.3 4.6-1.2 7-1h1c1.2-1.2 3.2-1.2 5-1h1c1.2-1.2 3.2-1.2 5-1h1c1.1-1.1 2.3-1 4-1h2c3.5-1.7 6.9-2.3 11-3l4-1c3.4-1.4 7.1-3 11-4 1.5-.4 2.5-.5 4-1 1.4-.7 2-1.9 4-2h1q2.6-2.1 6-3h1c2.5-2 6-3.8 9-5l3-1c1.4-.9 2-2.5 4-3h1q1.4-2.2 4-3h1c7.3-7.7 19-13.2 27.7-19.3 8.8-6.1 18.2-15 28.3-18.7.4-.2 1 0 1 0q3.8-3.9 9-6c1.3 2.5-.5 6.7-1 10m-20 55c-.2.4 0 1 0 1-3.4 9.6-12.7 19-19 27a88 88 0 0 1-12 12 214 214 0 0 1-26.7 20.3c-9.5 5.8-20 14.8-31.3 16.7h-1a22 22 0 0 1-10 4h-1c-3.2 2.6-8.9 3.3-13 4h-1q-1.5 1.4-4 1h-1q-1.5 1.4-4 1h-1c-4.9 2.3-10.5 1-16 2-1 .2-2.5 1-4 1-6.2.4-12.8.3-19 0-1.8 0-3.8-.8-5-1h-4c-1.6 0-3-.9-4-1h-4c-3.9-.3-8.8-1.3-12-3h-1c-3.3-.5-7.5-1-10-3h-1c-3.6-.1-8.4-1.8-11-4h-1c-3.9-.6-8-2.6-11-5h-1c-16.1-3.8-32.2-18.9-45-29a200 200 0 0 1-40-51c17.7 11.5 35 25.5 52 38h1c4 1.6 12.8 5.4 15 9h1c4.6 1 10.4 4.1 14 7h1q2.5.3 4 2h1c3.3.5 8.6 2 11 4h1q3.5 0 6 2h1q2.5-.5 4 1h1q2.5-.5 4 1h1c3.8-.2 7.9 1 11 2h9c1.6 0 3 .8 4 1h32c1.2-.2 3.2-1 5-1h8a139 139 0 0 1 20-4l5-1c2-.7 3.7-1.5 6-2l4-1c1.5-.6 3-1.7 5-2h1q3-2.4 7-3h1q2.6-2.1 6-3h1c11.7-9.4 27.6-14.6 39-25 11.6-10.3 25-18.5 37-28a15 15 0 0 1-5 10Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
"use client";
|
||||
import { PlusCircleIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { buttonVariants } from "./ui/button";
|
||||
import Ripple from "./ui/ripple";
|
||||
'use client'
|
||||
import { PlusCircleIcon } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { buttonVariants } from './ui/button'
|
||||
import Ripple from './ui/ripple'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "./ui/tooltip";
|
||||
} from './ui/tooltip'
|
||||
|
||||
export const Sponsors = () => {
|
||||
return (
|
||||
<div className="mt-20 flex flex-col justify-center gap-y-10 w-full ">
|
||||
<div className="mt-20 flex w-full flex-col justify-center gap-y-10 ">
|
||||
<div className="flex flex-col justify-start gap-4 px-4">
|
||||
<h3 className="mx-auto max-w-2xl font-display text-3xl font-medium tracking-tight text-primary sm:text-5xl text-center">
|
||||
<h3 className="mx-auto max-w-2xl text-center font-display text-3xl font-medium tracking-tight text-primary sm:text-5xl">
|
||||
Sponsors
|
||||
</h3>
|
||||
<p className="mx-auto max-w-2xl text-lg tracking-tight text-muted-foreground text-center">
|
||||
Dokploy is an open source project that is maintained by a community of volunteers. We would like to thank our sponsors for their support and contributions to the project, which help us to continue to develop and improve Dokploy.
|
||||
<p className="mx-auto max-w-2xl text-center text-lg tracking-tight text-muted-foreground">
|
||||
Dokploy is an open source project that is maintained by a
|
||||
community of volunteers. We would like to thank our sponsors
|
||||
for their support and contributions to the project, which
|
||||
help us to continue to develop and improve Dokploy.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative flex h-[700px] w-full flex-col items-center justify-center overflow-hidden bg-background md:shadow-xl">
|
||||
@@ -26,18 +29,19 @@ export const Sponsors = () => {
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="z-10 m-0 p-0">
|
||||
<Link
|
||||
href={"https://opencollective.com/dokploy"}
|
||||
href={'https://opencollective.com/dokploy'}
|
||||
target="_blank"
|
||||
className={buttonVariants({
|
||||
variant: "secondary",
|
||||
size: "sm",
|
||||
className: "bg-transparent !rounded-full w-fit !p-0 m-0",
|
||||
variant: 'secondary',
|
||||
size: 'sm',
|
||||
className:
|
||||
'm-0 w-fit !rounded-full bg-transparent !p-0',
|
||||
})}
|
||||
>
|
||||
<PlusCircleIcon className="size-10 text-muted-foreground hover:text-primary transition-colors" />
|
||||
<PlusCircleIcon className="size-10 text-muted-foreground transition-colors hover:text-primary" />
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-black rounded-lg border-0 text-center w-[200px] z-[200] text-white font-semibold">
|
||||
<TooltipContent className="z-[200] w-[200px] rounded-lg border-0 bg-black text-center font-semibold text-white">
|
||||
Become a sponsor 🤑
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -45,5 +49,5 @@ export const Sponsors = () => {
|
||||
<Ripple />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,81 +1,82 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { HandCoins, Users } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useId } from "react";
|
||||
import NumberTicker from "./ui/number-ticker";
|
||||
import { HandCoins, Users } from 'lucide-react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useId } from 'react'
|
||||
import NumberTicker from './ui/number-ticker'
|
||||
|
||||
const statsValues = {
|
||||
githubStars: 26000,
|
||||
dockerDownloads: 4000000,
|
||||
contributors: 200,
|
||||
sponsors: 50,
|
||||
};
|
||||
}
|
||||
|
||||
export function StatsSection() {
|
||||
const [githubStars, setGithubStars] = useState(statsValues.githubStars);
|
||||
const [githubStars, setGithubStars] = useState(statsValues.githubStars)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchGitHubStars = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
"/api/github-stars?owner=dokploy&repo=dokploy",
|
||||
);
|
||||
'/api/github-stars?owner=dokploy&repo=dokploy',
|
||||
)
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setGithubStars(data.stargazers_count);
|
||||
const data = await response.json()
|
||||
setGithubStars(data.stargazers_count)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching GitHub stars:", error);
|
||||
console.error('Error fetching GitHub stars:', error)
|
||||
// Keep default value on error
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fetchGitHubStars();
|
||||
}, []);
|
||||
fetchGitHubStars()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="py-20 lg:py-40 flex flex-col gap-10 px-4 ">
|
||||
<div className="flex flex-col gap-10 px-4 py-20 lg:py-40 ">
|
||||
<div className="mx-auto max-w-2xl md:text-center">
|
||||
<h2 className="font-display text-3xl tracking-tight sm:text-4xl text-center">
|
||||
<h2 className="text-center font-display text-3xl tracking-tight sm:text-4xl">
|
||||
Stats You Didn't Ask For (But Secretly Love to See)
|
||||
</h2>
|
||||
<p className="mt-4 text-lg tracking-tight text-muted-foreground text-center">
|
||||
Just a few numbers to show we're not *completely* making this up.
|
||||
Turns out, Dokploy has actually helped a few people—who knew?
|
||||
<p className="mt-4 text-center text-lg tracking-tight text-muted-foreground">
|
||||
Just a few numbers to show we're not *completely* making
|
||||
this up. Turns out, Dokploy has actually helped a few
|
||||
people—who knew?
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-10 md:gap-2 max-w-7xl mx-auto">
|
||||
<div className="mx-auto grid max-w-7xl grid-cols-1 gap-10 sm:grid-cols-2 md:grid-cols-3 md:gap-2 lg:grid-cols-4">
|
||||
{grid.map((feature, index) => (
|
||||
<div
|
||||
key={feature.title}
|
||||
className="relative bg-gradient-to-b from-neutral-900 to-neutral-950 p-6 rounded-3xl overflow-hidden"
|
||||
className="relative overflow-hidden rounded-3xl bg-gradient-to-b from-neutral-900 to-neutral-950 p-6"
|
||||
>
|
||||
<Grid size={20} />
|
||||
|
||||
<p className="text-base font-bold text-white relative z-20 flex flex-row gap-4 items-center">
|
||||
<p className="relative z-20 flex flex-row items-center gap-4 text-base font-bold text-white">
|
||||
{feature.title}
|
||||
{feature.icon}
|
||||
</p>
|
||||
<p className="text-neutral-400 mt-4 text-base font-normal relative z-20">
|
||||
{typeof feature.description === "function"
|
||||
<p className="relative z-20 mt-4 text-base font-normal text-neutral-400">
|
||||
{typeof feature.description === 'function'
|
||||
? feature.description(githubStars)
|
||||
: feature.description}
|
||||
</p>
|
||||
{typeof feature.component === "function"
|
||||
{typeof feature.component === 'function'
|
||||
? feature.component(githubStars)
|
||||
: feature.component}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const grid = [
|
||||
{
|
||||
title: "GitHub Stars",
|
||||
title: 'GitHub Stars',
|
||||
description: (stars: number) =>
|
||||
`With over ${(stars / 1000).toFixed(1)}k stars on GitHub, Dokploy is trusted by developers worldwide. Explore our repositories and join our community!`,
|
||||
icon: (
|
||||
@@ -84,14 +85,14 @@ const grid = [
|
||||
</svg>
|
||||
),
|
||||
component: (stars: number) => (
|
||||
<p className="whitespace-pre-wrap text-2xl !font-semibold tracking-tighter mt-4">
|
||||
<p className="mt-4 whitespace-pre-wrap text-2xl !font-semibold tracking-tighter">
|
||||
<NumberTicker value={stars} />+
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "DockerHub Downloads",
|
||||
description: `Downloaded over ${(statsValues.dockerDownloads / 1000000).toFixed(2).split(".")[0]}M times, Dokploy has become a go-to solution for seamless deployments. Discover our presence on DockerHub.`,
|
||||
title: 'DockerHub Downloads',
|
||||
description: `Downloaded over ${(statsValues.dockerDownloads / 1000000).toFixed(2).split('.')[0]}M times, Dokploy has become a go-to solution for seamless deployments. Discover our presence on DockerHub.`,
|
||||
icon: (
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
@@ -105,39 +106,39 @@ const grid = [
|
||||
</svg>
|
||||
),
|
||||
component: (
|
||||
<p className="whitespace-pre-wrap text-2xl !font-semibold tracking-tighter mt-4">
|
||||
<p className="mt-4 whitespace-pre-wrap text-2xl !font-semibold tracking-tighter">
|
||||
<NumberTicker value={statsValues.dockerDownloads} />+
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Community Contributors",
|
||||
title: 'Community Contributors',
|
||||
description: `Thanks to our growing base of over ${statsValues.contributors} contributors, Dokploy continues to thrive, with valuable contributions from developers around the world.`,
|
||||
icon: <Users className="h-6 w-6 stroke-white" />,
|
||||
component: (
|
||||
<p className="whitespace-pre-wrap text-2xl !font-semibold tracking-tighter mt-4">
|
||||
<p className="mt-4 whitespace-pre-wrap text-2xl !font-semibold tracking-tighter">
|
||||
<NumberTicker value={statsValues.contributors} />+
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Sponsors",
|
||||
title: 'Sponsors',
|
||||
description: `More than ${statsValues.sponsors} companies/individuals have sponsored Dokploy, ensuring a steady flow of support and resources. Join our community!`,
|
||||
icon: <HandCoins className="h-6 w-6 stroke-white" />,
|
||||
component: (
|
||||
<p className="whitespace-pre-wrap text-2xl !font-semibold tracking-tighter mt-4">
|
||||
<p className="mt-4 whitespace-pre-wrap text-2xl !font-semibold tracking-tighter">
|
||||
<NumberTicker value={statsValues.sponsors} />+
|
||||
</p>
|
||||
),
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
export const Grid = ({
|
||||
pattern,
|
||||
size,
|
||||
}: {
|
||||
pattern?: number[][];
|
||||
size?: number;
|
||||
pattern?: number[][]
|
||||
size?: number
|
||||
}) => {
|
||||
const p = pattern ?? [
|
||||
[Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1],
|
||||
@@ -145,25 +146,25 @@ export const Grid = ({
|
||||
[Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1],
|
||||
[Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1],
|
||||
[Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1],
|
||||
];
|
||||
]
|
||||
return (
|
||||
<div className="pointer-events-none absolute left-1/2 top-0 -ml-20 -mt-2 h-full w-full [mask-image:linear-gradient(white,transparent)]">
|
||||
<div className="absolute inset-0 bg-gradient-to-r [mask-image:radial-gradient(farthest-side_at_top,white,transparent)] from-zinc-900/30 to-zinc-900/30 opacity-100">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-zinc-900/30 to-zinc-900/30 opacity-100 [mask-image:radial-gradient(farthest-side_at_top,white,transparent)]">
|
||||
<GridPattern
|
||||
width={size ?? 20}
|
||||
height={size ?? 20}
|
||||
x="-12"
|
||||
y="4"
|
||||
squares={p}
|
||||
className="absolute inset-0 h-full w-full mix-blend-overlay fill-white/10 stroke-white/10 "
|
||||
className="absolute inset-0 h-full w-full fill-white/10 stroke-white/10 mix-blend-overlay "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export function GridPattern({ width, height, x, y, squares, ...props }: any) {
|
||||
const patternId = useId();
|
||||
const patternId = useId()
|
||||
|
||||
return (
|
||||
<svg aria-hidden="true" {...props}>
|
||||
@@ -200,5 +201,5 @@ export function GridPattern({ width, height, x, y, squares, ...props }: any) {
|
||||
</svg>
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDown, Minus, PlusIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion'
|
||||
import { ChevronDown, Minus, PlusIcon } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
@@ -14,11 +14,11 @@ const AccordionItem = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
className={cn('border-b', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AccordionItem.displayName = "AccordionItem";
|
||||
))
|
||||
AccordionItem.displayName = 'AccordionItem'
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
@@ -28,7 +28,7 @@ const AccordionTrigger = React.forwardRef<
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180 group",
|
||||
'group flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -38,8 +38,8 @@ const AccordionTrigger = React.forwardRef<
|
||||
<Minus className="h-4 w-4 shrink-0 transition-transform duration-200 group-data-[state=closed]:hidden" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
@@ -50,12 +50,12 @@ const AccordionContent = React.forwardRef<
|
||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0 text-muted-foreground", className)}>
|
||||
<div className={cn('pb-4 pt-0 text-muted-foreground', className)}>
|
||||
{children}
|
||||
</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
))
|
||||
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export default function AnimatedGradientText({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-2xl px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#8fdfff1f] backdrop-blur-sm transition-shadow duration-500 ease-out [--bg-size:300%] hover:shadow-[inset_0_-5px_10px_#8fdfff3f] bg-black/40",
|
||||
'group relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-2xl bg-black/40 px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#8fdfff1f] backdrop-blur-sm transition-shadow duration-500 ease-out [--bg-size:300%] hover:shadow-[inset_0_-5px_10px_#8fdfff3f]',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"absolute inset-0 block h-full w-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] p-[1px] ![mask-composite:subtract] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]"
|
||||
'absolute inset-0 block h-full w-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] p-[1px] [border-radius:inherit] ![mask-composite:subtract] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]'
|
||||
}
|
||||
/>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useId, useRef, useState } from "react";
|
||||
import { motion } from 'framer-motion'
|
||||
import { useEffect, useId, useRef, useState } from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface GridPatternProps {
|
||||
width?: number;
|
||||
height?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
strokeDasharray?: any;
|
||||
numSquares?: number;
|
||||
className?: string;
|
||||
maxOpacity?: number;
|
||||
duration?: number;
|
||||
repeatDelay?: number;
|
||||
width?: number
|
||||
height?: number
|
||||
x?: number
|
||||
y?: number
|
||||
strokeDasharray?: any
|
||||
numSquares?: number
|
||||
className?: string
|
||||
maxOpacity?: number
|
||||
duration?: number
|
||||
repeatDelay?: number
|
||||
}
|
||||
|
||||
export function GridPattern({
|
||||
@@ -31,16 +31,16 @@ export function GridPattern({
|
||||
repeatDelay = 0.5,
|
||||
...props
|
||||
}: GridPatternProps) {
|
||||
const id = useId();
|
||||
const containerRef = useRef(null);
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
const [squares, setSquares] = useState(() => generateSquares(numSquares));
|
||||
const id = useId()
|
||||
const containerRef = useRef(null)
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
|
||||
const [squares, setSquares] = useState(() => generateSquares(numSquares))
|
||||
|
||||
function getPos() {
|
||||
return [
|
||||
Math.floor((Math.random() * dimensions.width) / width),
|
||||
Math.floor((Math.random() * dimensions.height) / height),
|
||||
];
|
||||
]
|
||||
}
|
||||
|
||||
// Adjust the generateSquares function to return objects with an id, x, and y
|
||||
@@ -48,7 +48,7 @@ export function GridPattern({
|
||||
return Array.from({ length: count }, (_, i) => ({
|
||||
id: i,
|
||||
pos: getPos(),
|
||||
}));
|
||||
}))
|
||||
}
|
||||
|
||||
// Function to update a single square's position
|
||||
@@ -62,15 +62,15 @@ export function GridPattern({
|
||||
}
|
||||
: sq,
|
||||
),
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
// Update squares to animate in
|
||||
useEffect(() => {
|
||||
if (dimensions.width && dimensions.height) {
|
||||
setSquares(generateSquares(numSquares));
|
||||
setSquares(generateSquares(numSquares))
|
||||
}
|
||||
}, [dimensions, numSquares]);
|
||||
}, [dimensions, numSquares])
|
||||
|
||||
// Resize observer to update container dimensions
|
||||
useEffect(() => {
|
||||
@@ -79,27 +79,27 @@ export function GridPattern({
|
||||
setDimensions({
|
||||
width: entry.contentRect.width,
|
||||
height: entry.contentRect.height,
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
resizeObserver.observe(containerRef.current)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (containerRef.current) {
|
||||
resizeObserver.unobserve(containerRef.current);
|
||||
resizeObserver.unobserve(containerRef.current)
|
||||
}
|
||||
};
|
||||
}, [containerRef]);
|
||||
}
|
||||
}, [containerRef])
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={containerRef}
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/10",
|
||||
'pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/10',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -130,7 +130,7 @@ export function GridPattern({
|
||||
duration,
|
||||
repeat: 1,
|
||||
delay: index * 0.1,
|
||||
repeatType: "reverse",
|
||||
repeatType: 'reverse',
|
||||
}}
|
||||
onAnimationComplete={() => updateSquarePosition(id)}
|
||||
key={`${x}-${y}-${index}`}
|
||||
@@ -144,7 +144,7 @@ export function GridPattern({
|
||||
))}
|
||||
</svg>
|
||||
</svg>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default GridPattern;
|
||||
export default GridPattern
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { CSSProperties, FC, ReactNode } from "react";
|
||||
import type { CSSProperties, FC, ReactNode } from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface AnimatedShinyTextProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
shimmerWidth?: number;
|
||||
children: ReactNode
|
||||
className?: string
|
||||
shimmerWidth?: number
|
||||
}
|
||||
|
||||
const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
|
||||
@@ -17,24 +17,24 @@ const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
|
||||
<p
|
||||
style={
|
||||
{
|
||||
"--shiny-width": `${shimmerWidth}px`,
|
||||
'--shiny-width': `${shimmerWidth}px`,
|
||||
} as CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"mx-auto max-w-md text-neutral-600/70 dark:text-neutral-400/70",
|
||||
'mx-auto max-w-md text-neutral-600/70 dark:text-neutral-400/70',
|
||||
|
||||
// Shine effect
|
||||
"animate-shiny-text bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shiny-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]",
|
||||
'animate-shiny-text bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shiny-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',
|
||||
|
||||
// Shine gradient
|
||||
"bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80",
|
||||
'bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80',
|
||||
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default AnimatedShinyText;
|
||||
export default AnimatedShinyText
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
import * as React from "react";
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
@@ -12,13 +12,13 @@ const Avatar = React.forwardRef<
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
@@ -26,11 +26,11 @@ const AvatarImage = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
className={cn('aspect-square h-full w-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
@@ -39,12 +39,12 @@ const AvatarFallback = React.forwardRef<
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
'flex h-full w-full items-center justify-center rounded-full bg-muted',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback };
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||
outline: 'text-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
@@ -30,7 +30,7 @@ export interface BadgeProps
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
export { Badge, badgeVariants }
|
||||
|
||||
@@ -1,59 +1,60 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
import { cn } from '../../lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm will-change-transform transition-all active:hover:scale-[0.98] font-medium ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm will-change-transform transition-all active:hover:scale-[0.98] font-medium ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
default:
|
||||
'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-9 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-10 w-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
children?: React.ReactNode;
|
||||
asChild?: boolean
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
|
||||
export { Button, buttonVariants };
|
||||
export { Button, buttonVariants }
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { CheckIcon, CopyIcon } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
interface CopyButtonProps {
|
||||
text: string;
|
||||
text: string
|
||||
}
|
||||
|
||||
export function CopyButton({ text }: CopyButtonProps) {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
|
||||
const copy = async () => {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
};
|
||||
await navigator.clipboard.writeText(text)
|
||||
setIsCopied(true)
|
||||
setTimeout(() => setIsCopied(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -28,5 +28,5 @@ export function CopyButton({ text }: CopyButtonProps) {
|
||||
<CopyIcon className="h-full w-full text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
import { X } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
@@ -21,13 +21,13 @@ const DialogOverlay = React.forwardRef<
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
@@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -50,8 +50,8 @@ const DialogContent = React.forwardRef<
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
@@ -59,13 +59,13 @@ const DialogHeader = ({
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
'flex flex-col space-y-1.5 text-center sm:text-left',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
)
|
||||
DialogHeader.displayName = 'DialogHeader'
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
@@ -73,13 +73,13 @@ const DialogFooter = ({
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
)
|
||||
DialogFooter.displayName = 'DialogFooter'
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
@@ -88,13 +88,13 @@ const DialogTitle = React.forwardRef<
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
'text-lg font-semibold leading-none tracking-tight',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
@@ -102,11 +102,11 @@ const DialogDescription = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
@@ -119,4 +119,4 @@ export {
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,86 +1,86 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Play, XIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { Play, XIcon } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type AnimationStyle =
|
||||
| "from-bottom"
|
||||
| "from-center"
|
||||
| "from-top"
|
||||
| "from-left"
|
||||
| "from-right"
|
||||
| "fade"
|
||||
| "top-in-bottom-out"
|
||||
| "left-in-right-out";
|
||||
| 'from-bottom'
|
||||
| 'from-center'
|
||||
| 'from-top'
|
||||
| 'from-left'
|
||||
| 'from-right'
|
||||
| 'fade'
|
||||
| 'top-in-bottom-out'
|
||||
| 'left-in-right-out'
|
||||
|
||||
interface HeroVideoProps {
|
||||
animationStyle?: AnimationStyle;
|
||||
videoSrc: string;
|
||||
thumbnailSrc: string;
|
||||
thumbnailAlt?: string;
|
||||
className?: string;
|
||||
animationStyle?: AnimationStyle
|
||||
videoSrc: string
|
||||
thumbnailSrc: string
|
||||
thumbnailAlt?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const animationVariants = {
|
||||
"from-bottom": {
|
||||
initial: { y: "100%", opacity: 0 },
|
||||
'from-bottom': {
|
||||
initial: { y: '100%', opacity: 0 },
|
||||
animate: { y: 0, opacity: 1 },
|
||||
exit: { y: "100%", opacity: 0 },
|
||||
exit: { y: '100%', opacity: 0 },
|
||||
},
|
||||
"from-center": {
|
||||
'from-center': {
|
||||
initial: { scale: 0.5, opacity: 0 },
|
||||
animate: { scale: 1, opacity: 1 },
|
||||
exit: { scale: 0.5, opacity: 0 },
|
||||
},
|
||||
"from-top": {
|
||||
initial: { y: "-100%", opacity: 0 },
|
||||
'from-top': {
|
||||
initial: { y: '-100%', opacity: 0 },
|
||||
animate: { y: 0, opacity: 1 },
|
||||
exit: { y: "-100%", opacity: 0 },
|
||||
exit: { y: '-100%', opacity: 0 },
|
||||
},
|
||||
"from-left": {
|
||||
initial: { x: "-100%", opacity: 0 },
|
||||
'from-left': {
|
||||
initial: { x: '-100%', opacity: 0 },
|
||||
animate: { x: 0, opacity: 1 },
|
||||
exit: { x: "-100%", opacity: 0 },
|
||||
exit: { x: '-100%', opacity: 0 },
|
||||
},
|
||||
"from-right": {
|
||||
initial: { x: "100%", opacity: 0 },
|
||||
'from-right': {
|
||||
initial: { x: '100%', opacity: 0 },
|
||||
animate: { x: 0, opacity: 1 },
|
||||
exit: { x: "100%", opacity: 0 },
|
||||
exit: { x: '100%', opacity: 0 },
|
||||
},
|
||||
fade: {
|
||||
initial: { opacity: 0 },
|
||||
animate: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
},
|
||||
"top-in-bottom-out": {
|
||||
initial: { y: "-100%", opacity: 0 },
|
||||
'top-in-bottom-out': {
|
||||
initial: { y: '-100%', opacity: 0 },
|
||||
animate: { y: 0, opacity: 1 },
|
||||
exit: { y: "100%", opacity: 0 },
|
||||
exit: { y: '100%', opacity: 0 },
|
||||
},
|
||||
"left-in-right-out": {
|
||||
initial: { x: "-100%", opacity: 0 },
|
||||
'left-in-right-out': {
|
||||
initial: { x: '-100%', opacity: 0 },
|
||||
animate: { x: 0, opacity: 1 },
|
||||
exit: { x: "100%", opacity: 0 },
|
||||
exit: { x: '100%', opacity: 0 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function HeroVideoDialog({
|
||||
animationStyle = "from-center",
|
||||
animationStyle = 'from-center',
|
||||
videoSrc,
|
||||
thumbnailSrc,
|
||||
thumbnailAlt = "Video thumbnail",
|
||||
thumbnailAlt = 'Video thumbnail',
|
||||
className,
|
||||
}: HeroVideoProps) {
|
||||
const [isVideoOpen, setIsVideoOpen] = useState(false);
|
||||
const selectedAnimation = animationVariants[animationStyle];
|
||||
const [isVideoOpen, setIsVideoOpen] = useState(false)
|
||||
const selectedAnimation = animationVariants[animationStyle]
|
||||
|
||||
return (
|
||||
<div className={cn("relative", className)}>
|
||||
<div className={cn('relative', className)}>
|
||||
<div
|
||||
className="relative cursor-pointer group"
|
||||
className="group relative cursor-pointer"
|
||||
onClick={() => setIsVideoOpen(true)}
|
||||
>
|
||||
<img
|
||||
@@ -88,20 +88,19 @@ export default function HeroVideoDialog({
|
||||
alt={thumbnailAlt}
|
||||
width={1920}
|
||||
height={1080}
|
||||
className="w-full transition-all duration-200 group-hover:brightness-[0.8] ease-out rounded-md shadow-lg border"
|
||||
className="w-full rounded-md border shadow-lg transition-all duration-200 ease-out group-hover:brightness-[0.8]"
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center group-hover:scale-100 scale-[0.9] transition-all duration-200 ease-out rounded-2xl">
|
||||
<div className="bg-primary/10 flex items-center justify-center rounded-full backdrop-blur-md size-28">
|
||||
<div className="absolute inset-0 flex scale-[0.9] items-center justify-center rounded-2xl transition-all duration-200 ease-out group-hover:scale-100">
|
||||
<div className="flex size-28 items-center justify-center rounded-full bg-primary/10 backdrop-blur-md">
|
||||
<div
|
||||
className={
|
||||
"flex items-center justify-center bg-gradient-to-b from-primary/30 to-primary shadow-md rounded-full size-20 transition-all ease-out duration-200 relative group-hover:scale-[1.2] scale-100"
|
||||
'relative flex size-20 scale-100 items-center justify-center rounded-full bg-gradient-to-b from-primary/30 to-primary shadow-md transition-all duration-200 ease-out group-hover:scale-[1.2]'
|
||||
}
|
||||
>
|
||||
<Play
|
||||
className="size-8 text-white fill-white group-hover:scale-105 scale-100 transition-transform duration-200 ease-out"
|
||||
className="size-8 scale-100 fill-white text-white transition-transform duration-200 ease-out group-hover:scale-105"
|
||||
style={{
|
||||
filter:
|
||||
"drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06))",
|
||||
filter: 'drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06))',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -119,13 +118,17 @@ export default function HeroVideoDialog({
|
||||
>
|
||||
<motion.div
|
||||
{...selectedAnimation}
|
||||
transition={{ type: "spring", damping: 30, stiffness: 300 }}
|
||||
className="relative w-full max-w-4xl aspect-video mx-4 md:mx-0"
|
||||
transition={{
|
||||
type: 'spring',
|
||||
damping: 30,
|
||||
stiffness: 300,
|
||||
}}
|
||||
className="relative mx-4 aspect-video w-full max-w-4xl md:mx-0"
|
||||
>
|
||||
<motion.button className="absolute -top-16 right-0 text-white text-xl bg-neutral-900/50 ring-1 backdrop-blur-md rounded-full p-2 dark:bg-neutral-100/50 dark:text-black">
|
||||
<motion.button className="absolute -top-16 right-0 rounded-full bg-neutral-900/50 p-2 text-xl text-white ring-1 backdrop-blur-md dark:bg-neutral-100/50 dark:text-black">
|
||||
<XIcon className="size-5" />
|
||||
</motion.button>
|
||||
<div className="size-full border-2 border-white rounded-2xl overflow-hidden isolate z-[1] relative">
|
||||
<div className="relative isolate z-[1] size-full overflow-hidden rounded-2xl border-2 border-white">
|
||||
{/* biome-ignore lint/a11y/useIframeTitle: <explanation> */}
|
||||
<iframe
|
||||
src={videoSrc}
|
||||
@@ -139,5 +142,5 @@ export default function HeroVideoDialog({
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,76 +1,74 @@
|
||||
"use client";
|
||||
import type React from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
'use client'
|
||||
import type React from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from '@/lib/utils'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
type Direction = "TOP" | "LEFT" | "BOTTOM" | "RIGHT";
|
||||
type Direction = 'TOP' | 'LEFT' | 'BOTTOM' | 'RIGHT'
|
||||
|
||||
export function HoverBorderGradient({
|
||||
children,
|
||||
containerClassName,
|
||||
className,
|
||||
as: Tag = "button",
|
||||
as: Tag = 'button',
|
||||
duration = 1,
|
||||
clockwise = true,
|
||||
...props
|
||||
}: React.PropsWithChildren<
|
||||
{
|
||||
as?: React.ElementType;
|
||||
containerClassName?: string;
|
||||
className?: string;
|
||||
duration?: number;
|
||||
clockwise?: boolean;
|
||||
as?: React.ElementType
|
||||
containerClassName?: string
|
||||
className?: string
|
||||
duration?: number
|
||||
clockwise?: boolean
|
||||
} & React.HTMLAttributes<HTMLElement>
|
||||
>) {
|
||||
const [hovered, setHovered] = useState<boolean>(false);
|
||||
const [direction, setDirection] = useState<Direction>("TOP");
|
||||
const [hovered, setHovered] = useState<boolean>(false)
|
||||
const [direction, setDirection] = useState<Direction>('TOP')
|
||||
|
||||
const rotateDirection = (currentDirection: Direction): Direction => {
|
||||
const directions: Direction[] = ["TOP", "LEFT", "BOTTOM", "RIGHT"];
|
||||
const currentIndex = directions.indexOf(currentDirection);
|
||||
const directions: Direction[] = ['TOP', 'LEFT', 'BOTTOM', 'RIGHT']
|
||||
const currentIndex = directions.indexOf(currentDirection)
|
||||
const nextIndex = clockwise
|
||||
? (currentIndex - 1 + directions.length) % directions.length
|
||||
: (currentIndex + 1) % directions.length;
|
||||
return directions[nextIndex];
|
||||
};
|
||||
: (currentIndex + 1) % directions.length
|
||||
return directions[nextIndex]
|
||||
}
|
||||
|
||||
const movingMap: Record<Direction, string> = {
|
||||
TOP: "radial-gradient(20.7% 50% at 50% 0%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
|
||||
LEFT: "radial-gradient(16.6% 43.1% at 0% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
|
||||
BOTTOM:
|
||||
"radial-gradient(20.7% 50% at 50% 100%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
|
||||
RIGHT:
|
||||
"radial-gradient(16.2% 41.199999999999996% at 100% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
|
||||
};
|
||||
TOP: 'radial-gradient(20.7% 50% at 50% 0%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)',
|
||||
LEFT: 'radial-gradient(16.6% 43.1% at 0% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)',
|
||||
BOTTOM: 'radial-gradient(20.7% 50% at 50% 100%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)',
|
||||
RIGHT: 'radial-gradient(16.2% 41.199999999999996% at 100% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)',
|
||||
}
|
||||
|
||||
const highlight =
|
||||
"radial-gradient(75% 181.15942028985506% at 50% 50%, #3275F8 0%, rgba(255, 255, 255, 0) 100%)";
|
||||
'radial-gradient(75% 181.15942028985506% at 50% 50%, #3275F8 0%, rgba(255, 255, 255, 0) 100%)'
|
||||
|
||||
useEffect(() => {
|
||||
if (!hovered) {
|
||||
const interval = setInterval(() => {
|
||||
setDirection((prevState) => rotateDirection(prevState));
|
||||
}, duration * 1000);
|
||||
return () => clearInterval(interval);
|
||||
setDirection((prevState) => rotateDirection(prevState))
|
||||
}, duration * 1000)
|
||||
return () => clearInterval(interval)
|
||||
}
|
||||
}, [hovered]);
|
||||
}, [hovered])
|
||||
return (
|
||||
<Tag
|
||||
onMouseEnter={(event: React.MouseEvent<HTMLDivElement>) => {
|
||||
setHovered(true);
|
||||
setHovered(true)
|
||||
}}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
className={cn(
|
||||
"relative flex rounded-full border content-center bg-black/20 hover:bg-black/10 transition duration-500 dark:bg-white/20 items-center flex-col flex-nowrap gap-10 h-min justify-center overflow-visible p-px decoration-clone w-fit",
|
||||
'relative flex h-min w-fit flex-col flex-nowrap content-center items-center justify-center gap-10 overflow-visible rounded-full border bg-black/20 decoration-clone p-px transition duration-500 hover:bg-black/10 dark:bg-white/20',
|
||||
containerClassName,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"w-auto text-white z-10 bg-black px-4 py-2 rounded-[inherit]",
|
||||
'z-10 w-auto rounded-[inherit] bg-black px-4 py-2 text-white',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -78,13 +76,13 @@ export function HoverBorderGradient({
|
||||
</div>
|
||||
<motion.div
|
||||
className={cn(
|
||||
"flex-none inset-0 overflow-hidden absolute z-0 rounded-[inherit]",
|
||||
'absolute inset-0 z-0 flex-none overflow-hidden rounded-[inherit]',
|
||||
)}
|
||||
style={{
|
||||
filter: "blur(2px)",
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
filter: 'blur(2px)',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
initial={{ background: movingMap[direction] }}
|
||||
animate={{
|
||||
@@ -92,9 +90,9 @@ export function HoverBorderGradient({
|
||||
? [movingMap[direction], highlight]
|
||||
: movingMap[direction],
|
||||
}}
|
||||
transition={{ ease: "linear", duration: duration ?? 1 }}
|
||||
transition={{ ease: 'linear', duration: duration ?? 1 }}
|
||||
/>
|
||||
<div className="bg-black absolute z-1 flex-none inset-[2px] rounded-[100px]" />
|
||||
<div className="z-1 absolute inset-[2px] flex-none rounded-[100px] bg-black" />
|
||||
</Tag>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as React from "react";
|
||||
import { cn } from '@/lib/utils'
|
||||
import * as React from 'react'
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
errorMessage?: string;
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
@@ -14,7 +14,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
type={type}
|
||||
className={cn(
|
||||
// bg-gray
|
||||
"flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
'flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
@@ -26,26 +26,28 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
)
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
)
|
||||
Input.displayName = 'Input'
|
||||
|
||||
const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, errorMessage, ...props }, ref) => {
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
className={cn("text-left", className)}
|
||||
className={cn('text-left', className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
value={props.value === undefined ? undefined : String(props.value)}
|
||||
value={
|
||||
props.value === undefined ? undefined : String(props.value)
|
||||
}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
props.onChange?.(e);
|
||||
const value = e.target.value
|
||||
if (value === '') {
|
||||
props.onChange?.(e)
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
const number = Number.parseInt(value, 10)
|
||||
if (!Number.isNaN(number)) {
|
||||
const syntheticEvent = {
|
||||
...e,
|
||||
@@ -53,17 +55,17 @@ const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
...e.target,
|
||||
value: number,
|
||||
},
|
||||
};
|
||||
}
|
||||
props.onChange?.(
|
||||
syntheticEvent as unknown as React.ChangeEvent<HTMLInputElement>,
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
)
|
||||
},
|
||||
);
|
||||
NumberInput.displayName = "NumberInput";
|
||||
)
|
||||
NumberInput.displayName = 'NumberInput'
|
||||
|
||||
export { Input, NumberInput };
|
||||
export { Input, NumberInput }
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface MarqueeProps {
|
||||
className?: string;
|
||||
reverse?: boolean;
|
||||
pauseOnHover?: boolean;
|
||||
children?: React.ReactNode;
|
||||
vertical?: boolean;
|
||||
repeat?: number;
|
||||
[key: string]: any;
|
||||
className?: string
|
||||
reverse?: boolean
|
||||
pauseOnHover?: boolean
|
||||
children?: React.ReactNode
|
||||
vertical?: boolean
|
||||
repeat?: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export function Marquee({
|
||||
@@ -23,10 +23,10 @@ export function Marquee({
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
"group flex overflow-hidden p-2 [--duration:40s] [--gap:1rem] [gap:var(--gap)]",
|
||||
'group flex overflow-hidden p-2 [--duration:40s] [--gap:1rem] [gap:var(--gap)]',
|
||||
{
|
||||
"flex-row": !vertical,
|
||||
"flex-col": vertical,
|
||||
'flex-row': !vertical,
|
||||
'flex-col': vertical,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
@@ -36,16 +36,20 @@ export function Marquee({
|
||||
.map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn("flex shrink-0 justify-around [gap:var(--gap)]", {
|
||||
"animate-marquee flex-row": !vertical,
|
||||
"animate-marquee-vertical flex-col": vertical,
|
||||
"group-hover:[animation-play-state:paused]": pauseOnHover,
|
||||
"[animation-direction:reverse]": reverse,
|
||||
})}
|
||||
className={cn(
|
||||
'flex shrink-0 justify-around [gap:var(--gap)]',
|
||||
{
|
||||
'animate-marquee flex-row': !vertical,
|
||||
'animate-marquee-vertical flex-col': vertical,
|
||||
'group-hover:[animation-play-state:paused]':
|
||||
pauseOnHover,
|
||||
'[animation-direction:reverse]': reverse,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { useInView, useMotionValue, useSpring } from "framer-motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useInView, useMotionValue, useSpring } from 'framer-motion'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export default function NumberTicker({
|
||||
value,
|
||||
direction = "up",
|
||||
direction = 'up',
|
||||
delay = 0,
|
||||
className,
|
||||
decimalPlaces = 0,
|
||||
}: {
|
||||
value: number;
|
||||
direction?: "up" | "down";
|
||||
className?: string;
|
||||
delay?: number; // delay in s
|
||||
decimalPlaces?: number;
|
||||
value: number
|
||||
direction?: 'up' | 'down'
|
||||
className?: string
|
||||
delay?: number // delay in s
|
||||
decimalPlaces?: number
|
||||
}) {
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const motionValue = useMotionValue(direction === "down" ? value : 0);
|
||||
const ref = useRef<HTMLSpanElement>(null)
|
||||
const motionValue = useMotionValue(direction === 'down' ? value : 0)
|
||||
const springValue = useSpring(motionValue, {
|
||||
damping: 60,
|
||||
stiffness: 100,
|
||||
});
|
||||
const isInView = useInView(ref, { once: true, margin: "0px" });
|
||||
})
|
||||
const isInView = useInView(ref, { once: true, margin: '0px' })
|
||||
|
||||
useEffect(() => {
|
||||
isInView &&
|
||||
setTimeout(() => {
|
||||
motionValue.set(direction === "down" ? 0 : value);
|
||||
}, delay * 1000);
|
||||
}, [motionValue, isInView, delay, value, direction]);
|
||||
motionValue.set(direction === 'down' ? 0 : value)
|
||||
}, delay * 1000)
|
||||
}, [motionValue, isInView, delay, value, direction])
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
springValue.on("change", (latest) => {
|
||||
springValue.on('change', (latest) => {
|
||||
if (ref.current) {
|
||||
ref.current.textContent = Intl.NumberFormat("en-US", {
|
||||
ref.current.textContent = Intl.NumberFormat('en-US', {
|
||||
minimumFractionDigits: decimalPlaces,
|
||||
maximumFractionDigits: decimalPlaces,
|
||||
}).format(Number(latest.toFixed(decimalPlaces)));
|
||||
}).format(Number(latest.toFixed(decimalPlaces)))
|
||||
}
|
||||
}),
|
||||
[springValue, decimalPlaces],
|
||||
);
|
||||
)
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-block tabular-nums text-white tracking-wider",
|
||||
'inline-block tabular-nums tracking-wider text-white',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import Link from "next/link";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "./avatar";
|
||||
import { cn } from '@/lib/utils'
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
||||
import Link from 'next/link'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './avatar'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "./tooltip";
|
||||
} from './tooltip'
|
||||
interface RippleProps {
|
||||
mainCircleSize?: number;
|
||||
mainCircleOpacity?: number;
|
||||
numCircles?: number;
|
||||
className?: string;
|
||||
mainCircleSize?: number
|
||||
mainCircleOpacity?: number
|
||||
numCircles?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
type AvatarItem = {
|
||||
name: string;
|
||||
image: string;
|
||||
link: string;
|
||||
type: "hero" | "premium" | "elite" | "supporting" | "community";
|
||||
};
|
||||
name: string
|
||||
image: string
|
||||
link: string
|
||||
type: 'hero' | 'premium' | 'elite' | 'supporting' | 'community'
|
||||
}
|
||||
|
||||
const Ripple = React.memo(function Ripple({
|
||||
mainCircleSize = 210,
|
||||
@@ -33,87 +33,83 @@ const Ripple = React.memo(function Ripple({
|
||||
}: RippleProps) {
|
||||
const heroSponsors: AvatarItem[] = [
|
||||
{
|
||||
name: "Hostinger",
|
||||
image: "https://avatars.githubusercontent.com/u/2630767?s=200&v=4",
|
||||
link: "https://www.hostinger.com/vps-hosting?ref=dokploy",
|
||||
type: "hero",
|
||||
name: 'Hostinger',
|
||||
image: 'https://avatars.githubusercontent.com/u/2630767?s=200&v=4',
|
||||
link: 'https://www.hostinger.com/vps-hosting?ref=dokploy',
|
||||
type: 'hero',
|
||||
},
|
||||
{
|
||||
name: "Lxaer",
|
||||
image:
|
||||
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/.github/sponsors/lxaer.png",
|
||||
link: "https://www.lxaer.com?ref=dokploy",
|
||||
type: "hero",
|
||||
name: 'Lxaer',
|
||||
image: 'https://raw.githubusercontent.com/Dokploy/dokploy/canary/.github/sponsors/lxaer.png',
|
||||
link: 'https://www.lxaer.com?ref=dokploy',
|
||||
type: 'hero',
|
||||
},
|
||||
{
|
||||
name: "LambdaTest",
|
||||
image: "https://www.lambdatest.com/blue-logo.png",
|
||||
link: "https://www.lambdatest.com/?utm_source=dokploy&utm_medium=sponsor",
|
||||
type: "premium",
|
||||
name: 'LambdaTest',
|
||||
image: 'https://www.lambdatest.com/blue-logo.png',
|
||||
link: 'https://www.lambdatest.com/?utm_source=dokploy&utm_medium=sponsor',
|
||||
type: 'premium',
|
||||
},
|
||||
];
|
||||
]
|
||||
const premiumSponsors = [
|
||||
{
|
||||
name: "Supafort",
|
||||
image: "supafort.png",
|
||||
link: "https://supafort.com/?ref=dokploy",
|
||||
type: "premium",
|
||||
name: 'Supafort',
|
||||
image: 'supafort.png',
|
||||
link: 'https://supafort.com/?ref=dokploy',
|
||||
type: 'premium',
|
||||
},
|
||||
{
|
||||
name: "AgentDock",
|
||||
image:
|
||||
"https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/.github/sponsors/agentdock.png",
|
||||
link: "https://agentdock.ai/?ref=dokploy",
|
||||
type: "premium",
|
||||
name: 'AgentDock',
|
||||
image: 'https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/.github/sponsors/agentdock.png',
|
||||
link: 'https://agentdock.ai/?ref=dokploy',
|
||||
type: 'premium',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const eliteSponsors = [
|
||||
{
|
||||
name: "AmericanCloud",
|
||||
image:
|
||||
"https://media.licdn.com/dms/image/v2/D560BAQGQ0rVfEgLUMQ/company-logo_200_200/company-logo_200_200/0/1722459194382/americancloud_logo?e=2147483647&v=beta&t=990H-OldnorPQbgbN3jHihJijOb2aDmhwFl8DU_d680",
|
||||
link: "https://americancloud.com/?ref=dokploy",
|
||||
type: "elite",
|
||||
name: 'AmericanCloud',
|
||||
image: 'https://media.licdn.com/dms/image/v2/D560BAQGQ0rVfEgLUMQ/company-logo_200_200/company-logo_200_200/0/1722459194382/americancloud_logo?e=2147483647&v=beta&t=990H-OldnorPQbgbN3jHihJijOb2aDmhwFl8DU_d680',
|
||||
link: 'https://americancloud.com/?ref=dokploy',
|
||||
type: 'elite',
|
||||
},
|
||||
{
|
||||
name: "Tolgee",
|
||||
image: "tolgee-logo.png",
|
||||
link: "https://tolg.ee/hrszh9",
|
||||
type: "elite",
|
||||
name: 'Tolgee',
|
||||
image: 'tolgee-logo.png',
|
||||
link: 'https://tolg.ee/hrszh9',
|
||||
type: 'elite',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const supportingSponsors = [
|
||||
{
|
||||
name: "Cloudblast",
|
||||
image: "https://cloudblast.io/img/logo-icon.193cf13e.svg",
|
||||
link: "https://cloudblast.io/?ref=dokploy",
|
||||
type: "supporting",
|
||||
name: 'Cloudblast',
|
||||
image: 'https://cloudblast.io/img/logo-icon.193cf13e.svg',
|
||||
link: 'https://cloudblast.io/?ref=dokploy',
|
||||
type: 'supporting',
|
||||
},
|
||||
|
||||
{
|
||||
name: "Synexa",
|
||||
image:
|
||||
"https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/.github/sponsors/synexa.png",
|
||||
link: "https://synexa.ai/?ref=dokploy",
|
||||
type: "supporting",
|
||||
name: 'Synexa',
|
||||
image: 'https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/.github/sponsors/synexa.png',
|
||||
link: 'https://synexa.ai/?ref=dokploy',
|
||||
type: 'supporting',
|
||||
},
|
||||
{
|
||||
name: "HahuCloud",
|
||||
image: "hahucloud_logo_1.png",
|
||||
link: "https://www.hahucloud.com/?ref=dokploy",
|
||||
type: "supporting",
|
||||
name: 'HahuCloud',
|
||||
image: 'hahucloud_logo_1.png',
|
||||
link: 'https://www.hahucloud.com/?ref=dokploy',
|
||||
type: 'supporting',
|
||||
},
|
||||
{
|
||||
name: "Teramont",
|
||||
image: "terramont.ico",
|
||||
link: "https://teramont.net/dokploy",
|
||||
type: "supporting",
|
||||
name: 'Teramont',
|
||||
image: 'terramont.ico',
|
||||
link: 'https://teramont.net/dokploy',
|
||||
type: 'supporting',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const communitySponsors = [];
|
||||
const communitySponsors = []
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -124,84 +120,104 @@ const Ripple = React.memo(function Ripple({
|
||||
>
|
||||
<div>
|
||||
{Array.from({ length: numCircles }, (_, i) => {
|
||||
const size = mainCircleSize + i * 70;
|
||||
const opacity = mainCircleOpacity - i * 0.03;
|
||||
const animationDelay = `${i * 0.06}s`;
|
||||
const borderStyle = i === numCircles - 1 ? "dashed" : "solid";
|
||||
const borderOpacity = 5 + i * 5;
|
||||
const size = mainCircleSize + i * 70
|
||||
const opacity = mainCircleOpacity - i * 0.03
|
||||
const animationDelay = `${i * 0.06}s`
|
||||
const borderStyle =
|
||||
i === numCircles - 1 ? 'dashed' : 'solid'
|
||||
const borderOpacity = 5 + i * 5
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={`absolute animate-ripple rounded-full bg-foreground/25 shadow-xl border [--i:${i}]`}
|
||||
className={`absolute animate-ripple rounded-full border bg-foreground/25 shadow-xl [--i:${i}]`}
|
||||
style={{
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
opacity,
|
||||
animationDelay,
|
||||
borderStyle,
|
||||
borderWidth: "1px",
|
||||
borderWidth: '1px',
|
||||
borderColor: `hsl(var(--foreground), ${borderOpacity / 100})`,
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%) scale(1)",
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%) scale(1)',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
)
|
||||
})}
|
||||
{Array.from({ length: numCircles }, (_, i) => {
|
||||
const size = mainCircleSize + i * 70;
|
||||
const opacity = mainCircleOpacity - i * 0.03;
|
||||
const animationDelay = `${i * 0.06}s`;
|
||||
const borderStyle = i === numCircles - 1 ? "dashed" : "solid";
|
||||
const borderOpacity = 5 + i * 5;
|
||||
const size = mainCircleSize + i * 70
|
||||
const opacity = mainCircleOpacity - i * 0.03
|
||||
const animationDelay = `${i * 0.06}s`
|
||||
const borderStyle =
|
||||
i === numCircles - 1 ? 'dashed' : 'solid'
|
||||
const borderOpacity = 5 + i * 5
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={`absolute z-30 animate-ripple rounded-full shadow-xl border [--i:${i}]`}
|
||||
className={`absolute z-30 animate-ripple rounded-full border shadow-xl [--i:${i}]`}
|
||||
style={{
|
||||
animationDelay,
|
||||
borderStyle,
|
||||
borderWidth: "1px",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%) scale(1)",
|
||||
borderWidth: '1px',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%) scale(1)',
|
||||
}}
|
||||
>
|
||||
{i === 0 && (
|
||||
<div className="relative w-full h-full flex justify-center items-center">
|
||||
<div className="relative flex h-full w-full items-center justify-center">
|
||||
{heroSponsors.map((item, index) => {
|
||||
const angle = (360 / heroSponsors.length) * index;
|
||||
const radius = mainCircleSize / 2;
|
||||
const x = radius * Math.cos((angle * Math.PI) / 180);
|
||||
const y = radius * Math.sin((angle * Math.PI) / 180);
|
||||
const angle =
|
||||
(360 / heroSponsors.length) * index
|
||||
const radius = mainCircleSize / 2
|
||||
const x =
|
||||
radius *
|
||||
Math.cos((angle * Math.PI) / 180)
|
||||
const y =
|
||||
radius *
|
||||
Math.sin((angle * Math.PI) / 180)
|
||||
const initials = item.name
|
||||
.split(" ")
|
||||
.split(' ')
|
||||
.map((n) => n[0])
|
||||
.join("");
|
||||
.join('')
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute"
|
||||
style={{
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: `translate(${x}px, ${y}px) translate(-50%, -50%)`,
|
||||
}}
|
||||
>
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<TooltipProvider
|
||||
delayDuration={100}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Link href={item.link} target="_blank">
|
||||
<Link
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
>
|
||||
<Avatar className="border-2 border-red-600">
|
||||
<AvatarImage
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
src={
|
||||
item.image
|
||||
}
|
||||
alt={
|
||||
item.name
|
||||
}
|
||||
className="object-contain"
|
||||
/>
|
||||
<AvatarFallback>{initials}</AvatarFallback>
|
||||
<AvatarFallback>
|
||||
{
|
||||
initials
|
||||
}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
@@ -215,42 +231,61 @@ const Ripple = React.memo(function Ripple({
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{i === 1 && (
|
||||
<div className="relative w-full h-full flex justify-center items-center">
|
||||
<div className="relative flex h-full w-full items-center justify-center">
|
||||
{premiumSponsors.map((item, index) => {
|
||||
const angle = (360 / premiumSponsors.length) * index;
|
||||
const radius = mainCircleSize / 2 + 70;
|
||||
const x = radius * Math.cos((angle * Math.PI) / 180);
|
||||
const y = radius * Math.sin((angle * Math.PI) / 180);
|
||||
const angle =
|
||||
(360 / premiumSponsors.length) *
|
||||
index
|
||||
const radius = mainCircleSize / 2 + 70
|
||||
const x =
|
||||
radius *
|
||||
Math.cos((angle * Math.PI) / 180)
|
||||
const y =
|
||||
radius *
|
||||
Math.sin((angle * Math.PI) / 180)
|
||||
const initials = item.name
|
||||
.split(" ")
|
||||
.split(' ')
|
||||
.map((n) => n[0])
|
||||
.join("");
|
||||
.join('')
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute"
|
||||
style={{
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: `translate(${x}px, ${y}px) translate(-50%, -50%)`,
|
||||
}}
|
||||
>
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<TooltipProvider
|
||||
delayDuration={100}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Link href={item.link} target="_blank">
|
||||
<Link
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
>
|
||||
<Avatar className="border-2 border-yellow-500">
|
||||
<AvatarImage
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
src={
|
||||
item.image
|
||||
}
|
||||
alt={
|
||||
item.name
|
||||
}
|
||||
/>
|
||||
<AvatarFallback>{initials}</AvatarFallback>
|
||||
<AvatarFallback>
|
||||
{
|
||||
initials
|
||||
}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
@@ -264,42 +299,60 @@ const Ripple = React.memo(function Ripple({
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{i === 2 && (
|
||||
<div className="relative w-full h-full flex justify-center items-center">
|
||||
<div className="relative flex h-full w-full items-center justify-center">
|
||||
{eliteSponsors.map((item, index) => {
|
||||
const angle = (360 / eliteSponsors.length) * index;
|
||||
const radius = mainCircleSize / 2 + 100;
|
||||
const x = radius * Math.cos((angle * Math.PI) / 180);
|
||||
const y = radius * Math.sin((angle * Math.PI) / 180);
|
||||
const angle =
|
||||
(360 / eliteSponsors.length) * index
|
||||
const radius = mainCircleSize / 2 + 100
|
||||
const x =
|
||||
radius *
|
||||
Math.cos((angle * Math.PI) / 180)
|
||||
const y =
|
||||
radius *
|
||||
Math.sin((angle * Math.PI) / 180)
|
||||
const initials = item.name
|
||||
.split(" ")
|
||||
.split(' ')
|
||||
.map((n) => n[0])
|
||||
.join("");
|
||||
.join('')
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute"
|
||||
style={{
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: `translate(${x}px, ${y}px) translate(-50%, -50%)`,
|
||||
}}
|
||||
>
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<TooltipProvider
|
||||
delayDuration={100}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Link href={item.link} target="_blank">
|
||||
<Link
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
>
|
||||
<Avatar className="border-2 border-yellow-900">
|
||||
<AvatarImage
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
src={
|
||||
item.image
|
||||
}
|
||||
alt={
|
||||
item.name
|
||||
}
|
||||
/>
|
||||
<AvatarFallback>{initials}</AvatarFallback>
|
||||
<AvatarFallback>
|
||||
{
|
||||
initials
|
||||
}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
@@ -313,41 +366,60 @@ const Ripple = React.memo(function Ripple({
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{i === 3 && (
|
||||
<div className="relative w-full h-full flex justify-center items-center">
|
||||
<div className="relative flex h-full w-full items-center justify-center">
|
||||
{supportingSponsors.map((item, index) => {
|
||||
const angle = (360 / supportingSponsors.length) * index;
|
||||
const radius = mainCircleSize / 2 + 140;
|
||||
const x = radius * Math.cos((angle * Math.PI) / 180);
|
||||
const y = radius * Math.sin((angle * Math.PI) / 180);
|
||||
const angle =
|
||||
(360 / supportingSponsors.length) *
|
||||
index
|
||||
const radius = mainCircleSize / 2 + 140
|
||||
const x =
|
||||
radius *
|
||||
Math.cos((angle * Math.PI) / 180)
|
||||
const y =
|
||||
radius *
|
||||
Math.sin((angle * Math.PI) / 180)
|
||||
const initials = item.name
|
||||
.split(" ")
|
||||
.split(' ')
|
||||
.map((n) => n[0])
|
||||
.join("");
|
||||
.join('')
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute"
|
||||
style={{
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: `translate(${x}px, ${y}px) translate(-50%, -50%)`,
|
||||
}}
|
||||
>
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<TooltipProvider
|
||||
delayDuration={100}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Link href={item.link} target="_blank">
|
||||
<Link
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
>
|
||||
<Avatar className="border-2 border-yellow-900">
|
||||
<AvatarImage
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
src={
|
||||
item.image
|
||||
}
|
||||
alt={
|
||||
item.name
|
||||
}
|
||||
/>
|
||||
<AvatarFallback>{initials}</AvatarFallback>
|
||||
<AvatarFallback>
|
||||
{
|
||||
initials
|
||||
}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
@@ -361,42 +433,61 @@ const Ripple = React.memo(function Ripple({
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{i === 4 && (
|
||||
<div className="relative w-full h-full flex justify-center items-center">
|
||||
<div className="relative flex h-full w-full items-center justify-center">
|
||||
{communitySponsors.map((item, index) => {
|
||||
const angle = (360 / communitySponsors.length) * index;
|
||||
const radius = mainCircleSize / 2 + 180;
|
||||
const x = radius * Math.cos((angle * Math.PI) / 180);
|
||||
const y = radius * Math.sin((angle * Math.PI) / 180);
|
||||
const angle =
|
||||
(360 / communitySponsors.length) *
|
||||
index
|
||||
const radius = mainCircleSize / 2 + 180
|
||||
const x =
|
||||
radius *
|
||||
Math.cos((angle * Math.PI) / 180)
|
||||
const y =
|
||||
radius *
|
||||
Math.sin((angle * Math.PI) / 180)
|
||||
const initials = item.name
|
||||
.split(" ")
|
||||
.split(' ')
|
||||
.map((n) => n[0])
|
||||
.join("");
|
||||
.join('')
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute"
|
||||
style={{
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: `translate(${x}px, ${y}px) translate(-50%, -50%)`,
|
||||
}}
|
||||
>
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<TooltipProvider
|
||||
delayDuration={100}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Link href={item.link} target="_blank">
|
||||
<Link
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
>
|
||||
<Avatar className="border-2 border-yellow-500">
|
||||
<AvatarImage
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
src={
|
||||
item.image
|
||||
}
|
||||
alt={
|
||||
item.name
|
||||
}
|
||||
/>
|
||||
<AvatarFallback>{initials}</AvatarFallback>
|
||||
<AvatarFallback>
|
||||
{
|
||||
initials
|
||||
}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
@@ -408,18 +499,18 @@ const Ripple = React.memo(function Ripple({
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
Ripple.displayName = "Ripple";
|
||||
Ripple.displayName = 'Ripple'
|
||||
|
||||
export default Ripple;
|
||||
export default Ripple
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { SVGProps } from "react";
|
||||
import type { SVGProps } from 'react'
|
||||
|
||||
export interface SafariProps extends SVGProps<SVGSVGElement> {
|
||||
url?: string;
|
||||
src?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
url?: string
|
||||
src?: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
export default function Safari({
|
||||
@@ -134,5 +134,5 @@ export default function Safari({
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
import * as React from "react";
|
||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
@@ -11,7 +11,7 @@ const ScrollArea = React.forwardRef<
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
@@ -20,29 +20,31 @@ const ScrollArea = React.forwardRef<
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
));
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
React.ComponentPropsWithoutRef<
|
||||
typeof ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
>
|
||||
>(({ className, orientation = 'vertical', ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
'flex touch-none select-none transition-colors',
|
||||
orientation === 'vertical' &&
|
||||
'h-full w-2.5 border-l border-l-transparent p-[1px]',
|
||||
orientation === 'horizontal' &&
|
||||
'h-2.5 flex-col border-t border-t-transparent p-[1px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
));
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar };
|
||||
export { ScrollArea, ScrollBar }
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import * as SelectPrimitive from '@radix-ui/react-select'
|
||||
import { Check, ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
@@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -29,8 +29,8 @@ const SelectTrigger = React.forwardRef<
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
@@ -39,15 +39,15 @@ const SelectScrollUpButton = React.forwardRef<
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
@@ -56,28 +56,28 @@ const SelectScrollDownButton = React.forwardRef<
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
))
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName;
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
>(({ className, children, position = 'popper', ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
position === 'popper' &&
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
@@ -86,9 +86,9 @@ const SelectContent = React.forwardRef<
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||
'p-1',
|
||||
position === 'popper' &&
|
||||
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -96,8 +96,8 @@ const SelectContent = React.forwardRef<
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
@@ -105,11 +105,11 @@ const SelectLabel = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
@@ -118,7 +118,7 @@ const SelectItem = React.forwardRef<
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -131,8 +131,8 @@ const SelectItem = React.forwardRef<
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
@@ -140,11 +140,11 @@ const SelectSeparator = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
@@ -157,4 +157,4 @@ export {
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
import * as React from "react";
|
||||
import * as SwitchPrimitives from '@radix-ui/react-switch'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
@@ -11,7 +11,7 @@ const Switch = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -19,11 +19,11 @@ const Switch = React.forwardRef<
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
|
||||
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0',
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
));
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch };
|
||||
export { Switch }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
import * as React from "react";
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
@@ -12,13 +12,13 @@ const TabsList = React.forwardRef<
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
@@ -27,13 +27,13 @@ const TabsTrigger = React.forwardRef<
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
@@ -42,12 +42,12 @@ const TabsContent = React.forwardRef<
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
@@ -19,12 +19,12 @@ const TooltipContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import GhostContentAPI from "@tryghost/content-api";
|
||||
import GhostContentAPI from '@tryghost/content-api'
|
||||
|
||||
// Ghost API configuration
|
||||
const ghostConfig = {
|
||||
url: process.env.GHOST_URL || "https://site.com",
|
||||
key: process.env.GHOST_KEY || "42424242424242424242424242424242",
|
||||
version: "v5.0",
|
||||
};
|
||||
url: process.env.GHOST_URL || 'https://site.com',
|
||||
key: process.env.GHOST_KEY || '42424242424242424242424242424242',
|
||||
version: 'v5.0',
|
||||
}
|
||||
|
||||
// Initialize the Ghost API with your credentials
|
||||
const api = GhostContentAPI({
|
||||
@@ -14,80 +14,80 @@ const api = GhostContentAPI({
|
||||
version: ghostConfig.version,
|
||||
// @ts-ignore
|
||||
makeRequest: ({ url, method, params, headers }) => {
|
||||
const apiUrl = new URL(url);
|
||||
const apiUrl = new URL(url)
|
||||
// @ts-ignore
|
||||
Object.keys(params).map((key) =>
|
||||
apiUrl.searchParams.set(key, encodeURIComponent(params[key])),
|
||||
);
|
||||
)
|
||||
|
||||
return fetch(apiUrl.toString(), { method, headers })
|
||||
.then(async (res) => {
|
||||
// Check if the response was successful.
|
||||
if (!res.ok) {
|
||||
// You can handle HTTP errors here
|
||||
throw new Error(`HTTP error! status: ${res.status}`);
|
||||
throw new Error(`HTTP error! status: ${res.status}`)
|
||||
}
|
||||
return { data: await res.json() };
|
||||
return { data: await res.json() }
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Fetch error:", error);
|
||||
});
|
||||
console.error('Fetch error:', error)
|
||||
})
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
export interface Post {
|
||||
id: string;
|
||||
uuid: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
html: string;
|
||||
feature_image: string | null;
|
||||
featured: boolean;
|
||||
visibility: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
published_at: string;
|
||||
custom_excerpt: string | null;
|
||||
excerpt: string;
|
||||
reading_time: number;
|
||||
id: string
|
||||
uuid: string
|
||||
title: string
|
||||
slug: string
|
||||
html: string
|
||||
feature_image: string | null
|
||||
featured: boolean
|
||||
visibility: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
published_at: string
|
||||
custom_excerpt: string | null
|
||||
excerpt: string
|
||||
reading_time: number
|
||||
primary_tag?: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
}
|
||||
tags?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}>;
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
}>
|
||||
primary_author?: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
profile_image: string | null;
|
||||
bio: string | null;
|
||||
twitter: string | null;
|
||||
};
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
profile_image: string | null
|
||||
bio: string | null
|
||||
twitter: string | null
|
||||
}
|
||||
authors?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
profile_image: string | null;
|
||||
bio: string | null;
|
||||
}>;
|
||||
url: string;
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
profile_image: string | null
|
||||
bio: string | null
|
||||
}>
|
||||
url: string
|
||||
}
|
||||
|
||||
export async function getPosts(options = {}): Promise<Post[]> {
|
||||
try {
|
||||
const result = (await api.posts.browse({
|
||||
include: "authors",
|
||||
limit: "all",
|
||||
})) as Post[];
|
||||
return result;
|
||||
include: 'authors',
|
||||
limit: 'all',
|
||||
})) as Post[]
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error("Error fetching posts:", error);
|
||||
return [];
|
||||
console.error('Error fetching posts:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,36 +95,36 @@ export async function getPost(slug: string): Promise<Post | null> {
|
||||
try {
|
||||
const result = (await api.posts.read({
|
||||
slug,
|
||||
include: ["authors"],
|
||||
})) as Post;
|
||||
include: ['authors'],
|
||||
})) as Post
|
||||
|
||||
return result;
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error("Error fetching post:", error);
|
||||
return null;
|
||||
console.error('Error fetching post:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTags() {
|
||||
try {
|
||||
const result = await api.tags.browse();
|
||||
return result;
|
||||
const result = await api.tags.browse()
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error("Error fetching tags:", error);
|
||||
return [];
|
||||
console.error('Error fetching tags:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPostsByTag(tag: string) {
|
||||
try {
|
||||
const result = await api.posts.browse({
|
||||
limit: "all",
|
||||
limit: 'all',
|
||||
filter: `tag:${tag}`,
|
||||
include: ["tags", "authors"],
|
||||
});
|
||||
return result;
|
||||
include: ['tags', 'authors'],
|
||||
})
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error(`Error fetching posts with tag ${tag}:`, error);
|
||||
return [];
|
||||
console.error(`Error fetching posts with tag ${tag}:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
export function useDebounce<T extends (...args: any[]) => any>(
|
||||
callback: T,
|
||||
delay: number,
|
||||
): T {
|
||||
const timeoutRef = useRef<NodeJS.Timeout>();
|
||||
const timeoutRef = useRef<NodeJS.Timeout>()
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
clearTimeout(timeoutRef.current)
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
}, [])
|
||||
|
||||
return ((...args: Parameters<T>) => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
clearTimeout(timeoutRef.current)
|
||||
}
|
||||
|
||||
return new Promise<ReturnType<T>>((resolve) => {
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
resolve(callback(...args));
|
||||
}, delay);
|
||||
});
|
||||
}) as T;
|
||||
resolve(callback(...args))
|
||||
}, delay)
|
||||
})
|
||||
}) as T
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
interface HubSpotFormField {
|
||||
objectTypeId: string;
|
||||
name: string;
|
||||
value: string;
|
||||
objectTypeId: string
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface HubSpotFormData {
|
||||
fields: HubSpotFormField[];
|
||||
fields: HubSpotFormField[]
|
||||
context: {
|
||||
pageUri: string;
|
||||
pageName: string;
|
||||
hutk?: string; // HubSpot UTK from cookies
|
||||
};
|
||||
pageUri: string
|
||||
pageName: string
|
||||
hutk?: string // HubSpot UTK from cookies
|
||||
}
|
||||
}
|
||||
|
||||
interface ContactFormData {
|
||||
inquiryType: "support" | "sales" | "other";
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
company: string;
|
||||
message: string;
|
||||
inquiryType: 'support' | 'sales' | 'other'
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
company: string
|
||||
message: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,19 +27,19 @@ interface ContactFormData {
|
||||
* This is used for tracking and attribution in HubSpot
|
||||
*/
|
||||
export function getHubSpotUTK(cookieHeader?: string): string | null {
|
||||
if (!cookieHeader) return null;
|
||||
if (!cookieHeader) return null
|
||||
|
||||
const name = "hubspotutk=";
|
||||
const decodedCookie = decodeURIComponent(cookieHeader);
|
||||
const cookieArray = decodedCookie.split(";");
|
||||
const name = 'hubspotutk='
|
||||
const decodedCookie = decodeURIComponent(cookieHeader)
|
||||
const cookieArray = decodedCookie.split(';')
|
||||
|
||||
for (let i = 0; i < cookieArray.length; i++) {
|
||||
const cookie = cookieArray[i].trim();
|
||||
const cookie = cookieArray[i].trim()
|
||||
if (cookie.indexOf(name) === 0) {
|
||||
return cookie.substring(name.length, cookie.length);
|
||||
return cookie.substring(name.length, cookie.length)
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,43 +52,43 @@ export function formatContactDataForHubSpot(
|
||||
const formData: HubSpotFormData = {
|
||||
fields: [
|
||||
{
|
||||
objectTypeId: "0-1", // Contact object type
|
||||
name: "firstname",
|
||||
objectTypeId: '0-1', // Contact object type
|
||||
name: 'firstname',
|
||||
value: contactData.firstName,
|
||||
},
|
||||
{
|
||||
objectTypeId: "0-1",
|
||||
name: "lastname",
|
||||
objectTypeId: '0-1',
|
||||
name: 'lastname',
|
||||
value: contactData.lastName,
|
||||
},
|
||||
{
|
||||
objectTypeId: "0-1",
|
||||
name: "email",
|
||||
objectTypeId: '0-1',
|
||||
name: 'email',
|
||||
value: contactData.email,
|
||||
},
|
||||
{
|
||||
objectTypeId: "0-1",
|
||||
name: "message",
|
||||
objectTypeId: '0-1',
|
||||
name: 'message',
|
||||
value: contactData.message,
|
||||
},
|
||||
{
|
||||
objectTypeId: "0-2", // Company object type
|
||||
name: "name",
|
||||
objectTypeId: '0-2', // Company object type
|
||||
name: 'name',
|
||||
value: contactData.company,
|
||||
},
|
||||
],
|
||||
context: {
|
||||
pageUri: "https://dokploy.com/contact",
|
||||
pageName: "Contact Us",
|
||||
pageUri: 'https://dokploy.com/contact',
|
||||
pageName: 'Contact Us',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Add HubSpot UTK if available
|
||||
if (hutk) {
|
||||
formData.context.hutk = hutk;
|
||||
formData.context.hutk = hutk
|
||||
}
|
||||
|
||||
return formData;
|
||||
return formData
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,40 +99,40 @@ export async function submitToHubSpot(
|
||||
hutk?: string | null,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const portalId = process.env.HUBSPOT_PORTAL_ID;
|
||||
const formGuid = process.env.HUBSPOT_FORM_GUID;
|
||||
const portalId = process.env.HUBSPOT_PORTAL_ID
|
||||
const formGuid = process.env.HUBSPOT_FORM_GUID
|
||||
|
||||
if (!portalId || !formGuid) {
|
||||
console.error(
|
||||
"HubSpot configuration missing: HUBSPOT_PORTAL_ID or HUBSPOT_FORM_GUID not set",
|
||||
);
|
||||
return false;
|
||||
'HubSpot configuration missing: HUBSPOT_PORTAL_ID or HUBSPOT_FORM_GUID not set',
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
const formData = formatContactDataForHubSpot(contactData, hutk);
|
||||
const formData = formatContactDataForHubSpot(contactData, hutk)
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.hsforms.com/submissions/v3/integration/submit/${portalId}/${formGuid}`,
|
||||
{
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("HubSpot API error:", response.status, errorText);
|
||||
return false;
|
||||
const errorText = await response.text()
|
||||
console.error('HubSpot API error:', response.status, errorText)
|
||||
return false
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log("HubSpot submission successful:", result);
|
||||
return true;
|
||||
const result = await response.json()
|
||||
console.log('HubSpot submission successful:', result)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error("Error submitting to HubSpot:", error);
|
||||
return false;
|
||||
console.error('Error submitting to HubSpot:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import satori from "satori";
|
||||
import sharp from "sharp";
|
||||
import * as fs from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import satori from 'satori'
|
||||
import sharp from 'sharp'
|
||||
|
||||
interface GenerateOGImageOptions {
|
||||
title: string;
|
||||
title: string
|
||||
author?: {
|
||||
name: string;
|
||||
image?: string;
|
||||
};
|
||||
date?: string;
|
||||
readingTime?: number;
|
||||
name: string
|
||||
image?: string
|
||||
}
|
||||
date?: string
|
||||
readingTime?: number
|
||||
}
|
||||
|
||||
// Logo de Dokploy como SVG string (versión simplificada)
|
||||
const DOKPLOY_LOGO = {
|
||||
type: "svg",
|
||||
type: 'svg',
|
||||
props: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
viewBox: "0 0 6323 5778",
|
||||
fill: "currentColor",
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
viewBox: '0 0 6323 5778',
|
||||
fill: 'currentColor',
|
||||
children: [
|
||||
{
|
||||
type: "path",
|
||||
type: 'path',
|
||||
props: {
|
||||
d: "M4638.51 44.5295C4616.52 81.8286 4611.45 115.575 4619.9 213.263C4636.82 433.505 4772.12 710.584 4924.33 842.019C5002.12 909.512 5196.61 1012.53 5245.66 1012.53C5284.56 1012.53 5282.87 1019.63 5213.53 1129.75C5140.8 1243.43 5024.11 1339.34 4890.5 1389.07C4743.36 1445.91 4455.85 1453.01 4234.3 1405.06C4016.13 1357.1 3931.57 1323.35 3211.11 977.006C2265.71 522.312 2253.87 516.984 2125.34 481.461C2017.1 451.267 1917.32 445.938 1316.93 435.281C853.533 428.177 601.539 429.953 538.964 444.162C334.325 485.013 156.745 632.434 70.4925 829.586C12.9907 961.021 -7.30411 1191.92 2.84328 1589.78C7.91697 1841.99 16.3731 1911.26 46.8153 2005.39C114.465 2213.2 226.086 2342.86 422.269 2445.88C1594.29 3055.1 1969.74 3206.07 2529.54 3294.88C2732.49 3326.85 3258.46 3330.4 3459.72 3303.76C3755.69 3261.13 4107.46 3161.66 4403.43 3033.78C4540.42 2975.17 4904.03 2776.24 5220.29 2587.97C5910.31 2177.68 6006.71 2111.96 6037.16 2030.26C6070.98 1934.35 5988.11 1811.79 5888.33 1811.79C5851.12 1811.79 5862.96 1806.47 5426.62 2069.34C4352.69 2715.85 4026.28 2865.05 3485.09 2957.41C3162.06 3014.24 2587.04 2987.6 2274.17 2902.35C1924.08 2806.44 1839.52 2770.91 1051.41 2383.71C552.493 2140.38 444.255 2079.99 395.209 2023.16C363.076 1984.08 336.016 1945.01 336.016 1934.35C336.016 1920.14 467.932 1916.59 787.575 1921.92L1240.82 1929.02L1435.32 2001.84C1541.86 2040.92 1744.81 2126.17 1883.49 2190.11C2296.15 2381.94 2610.72 2451.21 3058.9 2451.21C3490.16 2451.21 3872.38 2374.83 4305.33 2198.99C4910.8 1955.66 5342.06 1596.88 5545.01 1172.38C5565.3 1127.98 5585.6 1090.68 5587.29 1087.13C5590.67 1083.57 5660.01 1074.69 5742.88 1065.81C5940.76 1046.28 6084.51 978.782 6221.5 842.019L6322.97 740.779V520.536V302.071L6253.63 353.579C6177.53 412.192 6062.52 444.162 5920.46 444.162C5795.31 444.162 5661.7 508.104 5568.69 614.672L5497.65 692.823L5487.51 646.643C5451.99 500.999 5304.85 364.236 5115.44 300.294C4956.46 248.786 4893.88 206.159 4831.31 108.471C4800.87 64.0671 4770.42 21.4395 4760.28 14.335C4721.38 -14.0833 4665.57 1.90186 4638.51 44.5295ZM2057.69 806.496C2162.55 834.914 2250.49 873.99 2517.7 1007.2C2605.65 1051.6 2796.76 1142.19 2940.51 1211.46C3084.27 1280.73 3332.88 1397.95 3490.16 1472.55C3948.49 1691.02 4049.96 1726.54 4301.95 1754.96L4437.25 1770.94L4310.41 1833.11C4153.12 1911.26 4016.13 1960.99 3804.73 2016.05C3512.15 2090.65 3402.22 2104.86 3050.44 2104.86C2590.43 2103.08 2370.57 2056.9 1974.82 1872.18C1413.33 1611.09 1386.27 1603.99 801.104 1589.78C457.784 1580.9 356.311 1572.01 336.016 1552.48C278.514 1492.09 303.882 1019.63 373.223 914.841C412.121 854.452 474.697 806.496 552.493 779.854C577.862 770.973 904.27 767.421 1278.03 772.749C1814.15 778.078 1978.2 785.182 2057.69 806.496Z",
|
||||
d: 'M4638.51 44.5295C4616.52 81.8286 4611.45 115.575 4619.9 213.263C4636.82 433.505 4772.12 710.584 4924.33 842.019C5002.12 909.512 5196.61 1012.53 5245.66 1012.53C5284.56 1012.53 5282.87 1019.63 5213.53 1129.75C5140.8 1243.43 5024.11 1339.34 4890.5 1389.07C4743.36 1445.91 4455.85 1453.01 4234.3 1405.06C4016.13 1357.1 3931.57 1323.35 3211.11 977.006C2265.71 522.312 2253.87 516.984 2125.34 481.461C2017.1 451.267 1917.32 445.938 1316.93 435.281C853.533 428.177 601.539 429.953 538.964 444.162C334.325 485.013 156.745 632.434 70.4925 829.586C12.9907 961.021 -7.30411 1191.92 2.84328 1589.78C7.91697 1841.99 16.3731 1911.26 46.8153 2005.39C114.465 2213.2 226.086 2342.86 422.269 2445.88C1594.29 3055.1 1969.74 3206.07 2529.54 3294.88C2732.49 3326.85 3258.46 3330.4 3459.72 3303.76C3755.69 3261.13 4107.46 3161.66 4403.43 3033.78C4540.42 2975.17 4904.03 2776.24 5220.29 2587.97C5910.31 2177.68 6006.71 2111.96 6037.16 2030.26C6070.98 1934.35 5988.11 1811.79 5888.33 1811.79C5851.12 1811.79 5862.96 1806.47 5426.62 2069.34C4352.69 2715.85 4026.28 2865.05 3485.09 2957.41C3162.06 3014.24 2587.04 2987.6 2274.17 2902.35C1924.08 2806.44 1839.52 2770.91 1051.41 2383.71C552.493 2140.38 444.255 2079.99 395.209 2023.16C363.076 1984.08 336.016 1945.01 336.016 1934.35C336.016 1920.14 467.932 1916.59 787.575 1921.92L1240.82 1929.02L1435.32 2001.84C1541.86 2040.92 1744.81 2126.17 1883.49 2190.11C2296.15 2381.94 2610.72 2451.21 3058.9 2451.21C3490.16 2451.21 3872.38 2374.83 4305.33 2198.99C4910.8 1955.66 5342.06 1596.88 5545.01 1172.38C5565.3 1127.98 5585.6 1090.68 5587.29 1087.13C5590.67 1083.57 5660.01 1074.69 5742.88 1065.81C5940.76 1046.28 6084.51 978.782 6221.5 842.019L6322.97 740.779V520.536V302.071L6253.63 353.579C6177.53 412.192 6062.52 444.162 5920.46 444.162C5795.31 444.162 5661.7 508.104 5568.69 614.672L5497.65 692.823L5487.51 646.643C5451.99 500.999 5304.85 364.236 5115.44 300.294C4956.46 248.786 4893.88 206.159 4831.31 108.471C4800.87 64.0671 4770.42 21.4395 4760.28 14.335C4721.38 -14.0833 4665.57 1.90186 4638.51 44.5295ZM2057.69 806.496C2162.55 834.914 2250.49 873.99 2517.7 1007.2C2605.65 1051.6 2796.76 1142.19 2940.51 1211.46C3084.27 1280.73 3332.88 1397.95 3490.16 1472.55C3948.49 1691.02 4049.96 1726.54 4301.95 1754.96L4437.25 1770.94L4310.41 1833.11C4153.12 1911.26 4016.13 1960.99 3804.73 2016.05C3512.15 2090.65 3402.22 2104.86 3050.44 2104.86C2590.43 2103.08 2370.57 2056.9 1974.82 1872.18C1413.33 1611.09 1386.27 1603.99 801.104 1589.78C457.784 1580.9 356.311 1572.01 336.016 1552.48C278.514 1492.09 303.882 1019.63 373.223 914.841C412.121 854.452 474.697 806.496 552.493 779.854C577.862 770.973 904.27 767.421 1278.03 772.749C1814.15 778.078 1978.2 785.182 2057.69 806.496Z',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "path",
|
||||
type: 'path',
|
||||
props: {
|
||||
d: "M1266.2 1060.49C1173.18 1097.79 1129.21 1207.91 1171.49 1294.94C1222.22 1394.4 1332.15 1417.49 1413.33 1342.89C1477.6 1286.06 1479.29 1174.16 1418.41 1112C1374.44 1065.82 1308.48 1042.73 1266.2 1060.49Z",
|
||||
d: 'M1266.2 1060.49C1173.18 1097.79 1129.21 1207.91 1171.49 1294.94C1222.22 1394.4 1332.15 1417.49 1413.33 1342.89C1477.6 1286.06 1479.29 1174.16 1418.41 1112C1374.44 1065.82 1308.48 1042.73 1266.2 1060.49Z',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "path",
|
||||
type: 'path',
|
||||
props: {
|
||||
d: "M87.4063 2513.37C7.91846 2548.89 -8.99385 2616.39 4.536 2836.63C19.7571 3072.86 46.8168 3222.05 124.613 3488.48C427.344 4532.85 1129.2 5287.71 2106.74 5623.4C2641.17 5806.35 3236.48 5827.66 3752.3 5682.01C4596.23 5445.79 5315 4836.57 5692.15 4040.86C5886.64 3630.57 6018.55 3111.93 6018.55 2753.15C6018.55 2582.64 5991.49 2518.7 5910.31 2497.39C5820.68 2474.3 5575.45 2609.28 5164.48 2911.23C4484.61 3410.32 4229.23 3563.07 3890.98 3676.75C3635.61 3763.78 3466.49 3797.52 3194.2 3818.84C2651.31 3863.24 2057.69 3731.81 1570.62 3458.28C1394.73 3358.82 846.769 2980.5 581.246 2772.69C285.28 2540.01 270.059 2529.36 199.028 2508.04C155.056 2495.61 124.613 2497.39 87.4063 2513.37ZM5678.62 3076.41C5661.7 3138.57 5646.48 3202.52 5646.48 3218.5C5646.48 3236.26 5626.19 3262.9 5600.82 3280.67C5573.76 3296.65 5482.43 3371.25 5396.18 3445.85C5308.24 3518.67 5198.31 3611.03 5150.95 3650.1C5101.91 3689.18 4990.28 3781.54 4902.34 3856.14C4699.39 4026.65 4406.81 4236.23 4242.76 4330.37C4085.48 4420.95 3767.52 4532.85 3532.44 4582.58C2847.5 4724.67 2054.31 4570.15 1516.5 4190.05C1173.18 3946.72 412.123 3314.41 388.445 3254.02C363.077 3182.98 330.944 3042.66 337.708 3021.35C341.091 3012.47 417.196 3060.42 505.14 3129.69C1056.48 3559.52 1563.85 3863.24 1942.69 3992.9C2328.29 4124.34 2565.06 4163.41 2991.25 4163.41C3380.23 4163.41 3628.84 4126.11 3963.71 4012.44C4345.93 3884.56 4531.96 3781.54 5052.86 3405C5391.11 3161.66 5676.92 2968.06 5700.6 2966.29C5705.68 2966.29 5697.22 3016.02 5678.62 3076.41ZM5426.62 3881C5426.62 3886.33 5409.71 3925.41 5391.11 3966.26C5318.38 4115.45 5144.19 4364.11 5003.81 4518.64C4587.77 4973.33 4090.55 5271.73 3540.9 5392.5C3309.2 5444.01 2708.81 5440.46 2483.88 5387.17C1716.06 5204.23 1105.53 4754.87 696.249 4071.05C647.204 3987.57 609.997 3916.53 613.379 3912.97C616.762 3909.42 774.046 4028.42 965.155 4177.62C1154.57 4326.82 1371.05 4486.67 1443.77 4532.85C1974.82 4863.21 2463.59 4991.09 3118.09 4968C3461.41 4955.57 3691.42 4912.94 3997.53 4806.38C4357.76 4680.27 4623.29 4513.31 5130.66 4095.92C5382.65 3888.11 5426.62 3856.14 5426.62 3881Z",
|
||||
d: 'M87.4063 2513.37C7.91846 2548.89 -8.99385 2616.39 4.536 2836.63C19.7571 3072.86 46.8168 3222.05 124.613 3488.48C427.344 4532.85 1129.2 5287.71 2106.74 5623.4C2641.17 5806.35 3236.48 5827.66 3752.3 5682.01C4596.23 5445.79 5315 4836.57 5692.15 4040.86C5886.64 3630.57 6018.55 3111.93 6018.55 2753.15C6018.55 2582.64 5991.49 2518.7 5910.31 2497.39C5820.68 2474.3 5575.45 2609.28 5164.48 2911.23C4484.61 3410.32 4229.23 3563.07 3890.98 3676.75C3635.61 3763.78 3466.49 3797.52 3194.2 3818.84C2651.31 3863.24 2057.69 3731.81 1570.62 3458.28C1394.73 3358.82 846.769 2980.5 581.246 2772.69C285.28 2540.01 270.059 2529.36 199.028 2508.04C155.056 2495.61 124.613 2497.39 87.4063 2513.37ZM5678.62 3076.41C5661.7 3138.57 5646.48 3202.52 5646.48 3218.5C5646.48 3236.26 5626.19 3262.9 5600.82 3280.67C5573.76 3296.65 5482.43 3371.25 5396.18 3445.85C5308.24 3518.67 5198.31 3611.03 5150.95 3650.1C5101.91 3689.18 4990.28 3781.54 4902.34 3856.14C4699.39 4026.65 4406.81 4236.23 4242.76 4330.37C4085.48 4420.95 3767.52 4532.85 3532.44 4582.58C2847.5 4724.67 2054.31 4570.15 1516.5 4190.05C1173.18 3946.72 412.123 3314.41 388.445 3254.02C363.077 3182.98 330.944 3042.66 337.708 3021.35C341.091 3012.47 417.196 3060.42 505.14 3129.69C1056.48 3559.52 1563.85 3863.24 1942.69 3992.9C2328.29 4124.34 2565.06 4163.41 2991.25 4163.41C3380.23 4163.41 3628.84 4126.11 3963.71 4012.44C4345.93 3884.56 4531.96 3781.54 5052.86 3405C5391.11 3161.66 5676.92 2968.06 5700.6 2966.29C5705.68 2966.29 5697.22 3016.02 5678.62 3076.41ZM5426.62 3881C5426.62 3886.33 5409.71 3925.41 5391.11 3966.26C5318.38 4115.45 5144.19 4364.11 5003.81 4518.64C4587.77 4973.33 4090.55 5271.73 3540.9 5392.5C3309.2 5444.01 2708.81 5440.46 2483.88 5387.17C1716.06 5204.23 1105.53 4754.87 696.249 4071.05C647.204 3987.57 609.997 3916.53 613.379 3912.97C616.762 3909.42 774.046 4028.42 965.155 4177.62C1154.57 4326.82 1371.05 4486.67 1443.77 4532.85C1974.82 4863.21 2463.59 4991.09 3118.09 4968C3461.41 4955.57 3691.42 4912.94 3997.53 4806.38C4357.76 4680.27 4623.29 4513.31 5130.66 4095.92C5382.65 3888.11 5426.62 3856.14 5426.62 3881Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateOGImage({
|
||||
title,
|
||||
@@ -52,122 +52,122 @@ export async function generateOGImage({
|
||||
}: GenerateOGImageOptions): Promise<Buffer> {
|
||||
// Cargar la fuente
|
||||
const interRegular = await fs.readFile(
|
||||
join(process.cwd(), "public/fonts/Inter-Regular.ttf"),
|
||||
);
|
||||
join(process.cwd(), 'public/fonts/Inter-Regular.ttf'),
|
||||
)
|
||||
const interBold = await fs.readFile(
|
||||
join(process.cwd(), "public/fonts/Inter-Bold.ttf"),
|
||||
);
|
||||
join(process.cwd(), 'public/fonts/Inter-Bold.ttf'),
|
||||
)
|
||||
|
||||
// Crear el markup para la imagen OG
|
||||
const markup = {
|
||||
type: "div",
|
||||
type: 'div',
|
||||
props: {
|
||||
style: {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#000000",
|
||||
padding: "80px",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#000000',
|
||||
padding: '80px',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: "div",
|
||||
type: 'div',
|
||||
props: {
|
||||
style: {
|
||||
position: "absolute",
|
||||
left: "80px",
|
||||
top: "40px",
|
||||
fontSize: "32px",
|
||||
position: 'absolute',
|
||||
left: '80px',
|
||||
top: '40px',
|
||||
fontSize: '32px',
|
||||
fontWeight: 700,
|
||||
color: "#fff",
|
||||
color: '#fff',
|
||||
zIndex: 1,
|
||||
},
|
||||
children: "Dokploy - Blog Post",
|
||||
children: 'Dokploy - Blog Post',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "div",
|
||||
type: 'div',
|
||||
props: {
|
||||
style: {
|
||||
position: "absolute",
|
||||
right: "-50px",
|
||||
bottom: "-50px",
|
||||
width: "500px",
|
||||
height: "500px",
|
||||
position: 'absolute',
|
||||
right: '-50px',
|
||||
bottom: '-50px',
|
||||
width: '500px',
|
||||
height: '500px',
|
||||
opacity: 0.1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
transform: "rotate(-10deg)",
|
||||
color: "#ffffff",
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transform: 'rotate(-10deg)',
|
||||
color: '#ffffff',
|
||||
},
|
||||
children: DOKPLOY_LOGO,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "div",
|
||||
type: 'div',
|
||||
props: {
|
||||
style: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "24px",
|
||||
position: "relative",
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '24px',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: "div",
|
||||
type: 'div',
|
||||
props: {
|
||||
style: {
|
||||
fontSize: "64px",
|
||||
fontSize: '64px',
|
||||
fontWeight: 700,
|
||||
color: "#fff",
|
||||
color: '#fff',
|
||||
lineHeight: 1.2,
|
||||
maxWidth: "900px",
|
||||
maxWidth: '900px',
|
||||
},
|
||||
children: title,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "div",
|
||||
type: 'div',
|
||||
props: {
|
||||
style: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "16px",
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
},
|
||||
children: [
|
||||
author?.name && {
|
||||
type: "div",
|
||||
type: 'div',
|
||||
props: {
|
||||
style: {
|
||||
color: "#9CA3AF",
|
||||
fontSize: "24px",
|
||||
color: '#9CA3AF',
|
||||
fontSize: '24px',
|
||||
},
|
||||
children: author.name,
|
||||
},
|
||||
},
|
||||
date && {
|
||||
type: "div",
|
||||
type: 'div',
|
||||
props: {
|
||||
style: {
|
||||
color: "#9CA3AF",
|
||||
fontSize: "24px",
|
||||
color: '#9CA3AF',
|
||||
fontSize: '24px',
|
||||
},
|
||||
children: `• ${date}`,
|
||||
},
|
||||
},
|
||||
readingTime && {
|
||||
type: "div",
|
||||
type: 'div',
|
||||
props: {
|
||||
style: {
|
||||
color: "#9CA3AF",
|
||||
fontSize: "24px",
|
||||
color: '#9CA3AF',
|
||||
fontSize: '24px',
|
||||
},
|
||||
children: `• ${readingTime} min read`,
|
||||
},
|
||||
@@ -180,7 +180,7 @@ export async function generateOGImage({
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Generar SVG con Satori
|
||||
const svg = await satori(markup as any, {
|
||||
@@ -188,22 +188,22 @@ export async function generateOGImage({
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: "Inter",
|
||||
name: 'Inter',
|
||||
data: interRegular,
|
||||
weight: 400,
|
||||
style: "normal",
|
||||
style: 'normal',
|
||||
},
|
||||
{
|
||||
name: "Inter",
|
||||
name: 'Inter',
|
||||
data: interBold,
|
||||
weight: 700,
|
||||
style: "normal",
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
});
|
||||
})
|
||||
|
||||
// Convertir SVG a PNG
|
||||
const png = await sharp(Buffer.from(svg)).png().toBuffer();
|
||||
const png = await sharp(Buffer.from(svg)).png().toBuffer()
|
||||
|
||||
return png;
|
||||
return png
|
||||
}
|
||||
|
||||
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 {
|
||||
url: string;
|
||||
key: string;
|
||||
version: string;
|
||||
url: string
|
||||
key: string
|
||||
version: string
|
||||
}
|
||||
|
||||
interface BrowseOptions {
|
||||
limit?: string | number;
|
||||
page?: number;
|
||||
order?: string;
|
||||
filter?: string;
|
||||
include?: string | string[];
|
||||
fields?: string | string[];
|
||||
formats?: string | string[];
|
||||
limit?: string | number
|
||||
page?: number
|
||||
order?: string
|
||||
filter?: string
|
||||
include?: string | string[]
|
||||
fields?: string | string[]
|
||||
formats?: string | string[]
|
||||
}
|
||||
|
||||
interface ReadOptions {
|
||||
id?: string;
|
||||
slug?: string;
|
||||
include?: string | string[];
|
||||
fields?: string | string[];
|
||||
formats?: string | string[];
|
||||
id?: string
|
||||
slug?: string
|
||||
include?: string | string[]
|
||||
fields?: string | string[]
|
||||
formats?: string | string[]
|
||||
}
|
||||
|
||||
interface ApiObject {
|
||||
browse<T>(options?: BrowseOptions): Promise<T[]>;
|
||||
read<T>(options: ReadOptions): Promise<T>;
|
||||
browse<T>(options?: BrowseOptions): Promise<T[]>
|
||||
read<T>(options: ReadOptions): Promise<T>
|
||||
}
|
||||
|
||||
interface GhostAPI {
|
||||
posts: ApiObject;
|
||||
tags: ApiObject;
|
||||
authors: ApiObject;
|
||||
pages: ApiObject;
|
||||
posts: ApiObject
|
||||
tags: ApiObject
|
||||
authors: ApiObject
|
||||
pages: ApiObject
|
||||
settings: {
|
||||
browse<T>(): Promise<T>;
|
||||
};
|
||||
browse<T>(): Promise<T>
|
||||
}
|
||||
}
|
||||
|
||||
function GhostContentAPI(options: GhostContentAPIOptions): GhostAPI;
|
||||
function GhostContentAPI(options: GhostContentAPIOptions): GhostAPI
|
||||
|
||||
export default GhostContentAPI;
|
||||
export default GhostContentAPI
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
@@ -1,150 +1,153 @@
|
||||
import headlessuiPlugin from "@headlessui/tailwindcss";
|
||||
import type { Config } from "tailwindcss";
|
||||
import headlessuiPlugin from '@headlessui/tailwindcss'
|
||||
import type { Config } from 'tailwindcss'
|
||||
const config = {
|
||||
darkMode: ["class"],
|
||||
darkMode: ['class'],
|
||||
content: [
|
||||
"./pages/**/*.{ts,tsx}",
|
||||
"./components/**/*.{ts,tsx}",
|
||||
"./app/**/*.{ts,tsx}",
|
||||
"./src/**/*.{ts,tsx}",
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
prefix: "",
|
||||
prefix: '',
|
||||
theme: {
|
||||
fontSize: {
|
||||
xs: ["0.75rem", { lineHeight: "1rem" }],
|
||||
sm: ["0.875rem", { lineHeight: "1.5rem" }],
|
||||
base: ["1rem", { lineHeight: "1.75rem" }],
|
||||
lg: ["1.125rem", { lineHeight: "2rem" }],
|
||||
xl: ["1.25rem", { lineHeight: "2rem" }],
|
||||
"2xl": ["1.5rem", { lineHeight: "2rem" }],
|
||||
"3xl": ["2rem", { lineHeight: "2.5rem" }],
|
||||
"4xl": ["2.5rem", { lineHeight: "3.5rem" }],
|
||||
"5xl": ["3rem", { lineHeight: "3.5rem" }],
|
||||
"6xl": ["3.75rem", { lineHeight: "1" }],
|
||||
"7xl": ["4.5rem", { lineHeight: "1.1" }],
|
||||
"8xl": ["6rem", { lineHeight: "1" }],
|
||||
"9xl": ["8rem", { lineHeight: "1" }],
|
||||
xs: ['0.75rem', { lineHeight: '1rem' }],
|
||||
sm: ['0.875rem', { lineHeight: '1.5rem' }],
|
||||
base: ['1rem', { lineHeight: '1.75rem' }],
|
||||
lg: ['1.125rem', { lineHeight: '2rem' }],
|
||||
xl: ['1.25rem', { lineHeight: '2rem' }],
|
||||
'2xl': ['1.5rem', { lineHeight: '2rem' }],
|
||||
'3xl': ['2rem', { lineHeight: '2.5rem' }],
|
||||
'4xl': ['2.5rem', { lineHeight: '3.5rem' }],
|
||||
'5xl': ['3rem', { lineHeight: '3.5rem' }],
|
||||
'6xl': ['3.75rem', { lineHeight: '1' }],
|
||||
'7xl': ['4.5rem', { lineHeight: '1.1' }],
|
||||
'8xl': ['6rem', { lineHeight: '1' }],
|
||||
'9xl': ['8rem', { lineHeight: '1' }],
|
||||
},
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
padding: '2rem',
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
'2xl': '1400px',
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
"4xl": "2rem",
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
'4xl': '2rem',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: "var(--font-inter)",
|
||||
display: "var(--font-lexend)",
|
||||
sans: 'var(--font-inter)',
|
||||
display: 'var(--font-lexend)',
|
||||
},
|
||||
keyframes: {
|
||||
marquee: {
|
||||
from: {
|
||||
transform: "translateX(0)",
|
||||
transform: 'translateX(0)',
|
||||
},
|
||||
to: {
|
||||
transform: "translateX(calc(-100% - var(--gap)))",
|
||||
transform: 'translateX(calc(-100% - var(--gap)))',
|
||||
},
|
||||
},
|
||||
"marquee-vertical": {
|
||||
'marquee-vertical': {
|
||||
from: {
|
||||
transform: "translateY(0)",
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
to: {
|
||||
transform: "translateY(calc(-100% - var(--gap)))",
|
||||
transform: 'translateY(calc(-100% - var(--gap)))',
|
||||
},
|
||||
},
|
||||
"accordion-down": {
|
||||
'accordion-down': {
|
||||
from: {
|
||||
height: "0",
|
||||
height: '0',
|
||||
},
|
||||
to: {
|
||||
height: "var(--radix-accordion-content-height)",
|
||||
height: 'var(--radix-accordion-content-height)',
|
||||
},
|
||||
},
|
||||
"accordion-up": {
|
||||
'accordion-up': {
|
||||
from: {
|
||||
height: "var(--radix-accordion-content-height)",
|
||||
height: 'var(--radix-accordion-content-height)',
|
||||
},
|
||||
to: {
|
||||
height: "0",
|
||||
height: '0',
|
||||
},
|
||||
},
|
||||
"shiny-text": {
|
||||
"0%, 90%, 100%": {
|
||||
"background-position": "calc(-100% - var(--shiny-width)) 0",
|
||||
'shiny-text': {
|
||||
'0%, 90%, 100%': {
|
||||
'background-position':
|
||||
'calc(-100% - var(--shiny-width)) 0',
|
||||
},
|
||||
"30%, 60%": {
|
||||
"background-position": "calc(100% + var(--shiny-width)) 0",
|
||||
'30%, 60%': {
|
||||
'background-position':
|
||||
'calc(100% + var(--shiny-width)) 0',
|
||||
},
|
||||
},
|
||||
gradient: {
|
||||
to: {
|
||||
backgroundPosition: "var(--bg-size) 0",
|
||||
backgroundPosition: 'var(--bg-size) 0',
|
||||
},
|
||||
},
|
||||
ripple: {
|
||||
"0%, 100%": {
|
||||
transform: "translate(-50%, -50%) scale(1)",
|
||||
'0%, 100%': {
|
||||
transform: 'translate(-50%, -50%) scale(1)',
|
||||
},
|
||||
"50%": {
|
||||
transform: "translate(-50%, -50%) scale(0.9)",
|
||||
'50%': {
|
||||
transform: 'translate(-50%, -50%) scale(0.9)',
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
"shiny-text": "shiny-text 8s infinite",
|
||||
marquee: "marquee var(--duration) linear infinite",
|
||||
"marquee-vertical": "marquee-vertical var(--duration) linear infinite",
|
||||
gradient: "gradient 8s linear infinite",
|
||||
ripple: "ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite",
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
'shiny-text': 'shiny-text 8s infinite',
|
||||
marquee: 'marquee var(--duration) linear infinite',
|
||||
'marquee-vertical':
|
||||
'marquee-vertical var(--duration) linear infinite',
|
||||
gradient: 'gradient 8s linear infinite',
|
||||
ripple: 'ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate"), headlessuiPlugin],
|
||||
} satisfies Config;
|
||||
plugins: [require('tailwindcss-animate'), headlessuiPlugin],
|
||||
} satisfies Config
|
||||
|
||||
export default config;
|
||||
export default config
|
||||
|
||||
Reference in New Issue
Block a user