From af1b2dbd7a1ae61f211d37400b8daae5d5fe2a38 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 30 Nov 2025 01:46:48 -0600 Subject: [PATCH] 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. --- README.md | 1 - apps/docs/app/docs/[[...slug]]/page.tsx | 188 ++++---- apps/docs/app/docs/layout.tsx | 22 +- apps/docs/app/layout.config.tsx | 206 ++++----- apps/docs/app/layout.tsx | 26 +- apps/docs/app/robots.ts | 14 +- apps/docs/app/sitemap.ts | 26 +- apps/docs/lib/source.ts | 12 +- apps/docs/source.config.ts | 2 +- apps/docs/utils/metadata.ts | 44 +- apps/website/README.md | 5 +- apps/website/app/api/contact/route.ts | 92 ++-- apps/website/app/api/github-stars/route.ts | 54 ++- apps/website/app/api/og/route.ts | 45 +- .../app/blog/[slug]/components/CodeBlock.tsx | 94 ++-- .../app/blog/[slug]/components/Headings.tsx | 59 ++- .../[slug]/components/TableOfContents.tsx | 54 ++- .../blog/[slug]/components/ZoomableImage.tsx | 22 +- .../app/blog/[slug]/components/shared.ts | 18 +- apps/website/app/blog/[slug]/page.tsx | 256 ++++++----- .../app/blog/components/BlogPostCard.tsx | 76 +-- .../app/blog/components/SearchAndFilter.tsx | 74 +-- apps/website/app/blog/page.tsx | 91 ++-- apps/website/app/blog/tag/[tag]/page.tsx | 82 ++-- apps/website/app/changelog/page.tsx | 120 ++--- apps/website/app/contact/layout.tsx | 13 +- apps/website/app/contact/page.tsx | 229 ++++++---- apps/website/app/layout.tsx | 72 +-- apps/website/app/not-found.tsx | 6 +- apps/website/app/page.tsx | 31 +- apps/website/app/privacy/page.tsx | 106 +++-- apps/website/app/robots.ts | 10 +- apps/website/app/rss.xml/route.ts | 42 +- apps/website/app/sitemap.ts | 18 +- apps/website/app/terms/page.tsx | 204 +++++---- apps/website/components/CallToAction.tsx | 42 +- apps/website/components/Container.tsx | 11 +- apps/website/components/Faqs.tsx | 86 ++-- apps/website/components/Footer.tsx | 20 +- apps/website/components/GithubStars.tsx | 120 ++--- apps/website/components/Header.tsx | 73 +-- apps/website/components/Hero.tsx | 117 ++--- apps/website/components/NavLink.tsx | 20 +- apps/website/components/SecondaryFeatures.tsx | 136 +++--- apps/website/components/SlimLayout.tsx | 12 +- apps/website/components/Testimonials.tsx | 139 +++--- apps/website/components/analitycs/google.tsx | 14 +- apps/website/components/analitycs/index.ts | 24 +- apps/website/components/blog/BlogCard.tsx | 30 +- apps/website/components/features-second.tsx | 163 +++---- apps/website/components/first-features.tsx | 109 ++--- apps/website/components/navigation.tsx | 10 +- apps/website/components/pricing.tsx | 413 +++++++++-------- .../website/components/secondary-features.tsx | 141 +++--- apps/website/components/shared/Logo.tsx | 4 +- apps/website/components/sponsors.tsx | 40 +- apps/website/components/stats.tsx | 93 ++-- apps/website/components/ui/accordion.tsx | 32 +- .../components/ui/animated-gradient-text.tsx | 14 +- .../components/ui/animated-grid-pattern.tsx | 68 +-- .../components/ui/animated-shiny-text.tsx | 24 +- apps/website/components/ui/avatar.tsx | 28 +- apps/website/components/ui/badge.tsx | 24 +- apps/website/components/ui/button.tsx | 53 +-- apps/website/components/ui/copy-button.tsx | 20 +- apps/website/components/ui/dialog.tsx | 56 +-- .../components/ui/hero-video-dialog.tsx | 115 ++--- .../components/ui/hover-border-gradient.tsx | 82 ++-- apps/website/components/ui/input.tsx | 40 +- apps/website/components/ui/marquee.tsx | 40 +- apps/website/components/ui/number-ticker.tsx | 46 +- apps/website/components/ui/ripple.tsx | 431 +++++++++++------- apps/website/components/ui/safari.tsx | 12 +- apps/website/components/ui/scroll-area.tsx | 36 +- apps/website/components/ui/select.tsx | 72 +-- apps/website/components/ui/switch.tsx | 18 +- apps/website/components/ui/tabs.tsx | 28 +- apps/website/components/ui/tooltip.tsx | 22 +- apps/website/lib/ghost.ts | 138 +++--- apps/website/lib/hooks/use-debounce.ts | 20 +- apps/website/lib/hubspot.ts | 106 ++--- apps/website/lib/og-image.ts | 170 +++---- apps/website/lib/types/ghost-content-api.d.ts | 52 +-- apps/website/lib/utils.ts | 8 +- apps/website/tailwind.config.ts | 161 +++---- 85 files changed, 3279 insertions(+), 2868 deletions(-) diff --git a/README.md b/README.md index f40e0e7..cdae681 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/apps/docs/app/docs/[[...slug]]/page.tsx b/apps/docs/app/docs/[[...slug]]/page.tsx index f235418..1527077 100644 --- a/apps/docs/app/docs/[[...slug]]/page.tsx +++ b/apps/docs/app/docs/[[...slug]]/page.tsx @@ -4,114 +4,114 @@ import { baseUrl } from "@/utils/metadata"; import { ImageZoom } from "fumadocs-ui/components/image-zoom"; import defaultMdxComponents from "fumadocs-ui/mdx"; import { - DocsBody, - DocsDescription, - DocsPage, - DocsTitle, + DocsBody, + DocsDescription, + DocsPage, + DocsTitle, } from "fumadocs-ui/page"; import { notFound, permanentRedirect } from "next/navigation"; export default async function Page(props: { - params: Promise<{ slug?: string[] }>; + params: Promise<{ slug?: string[] }>; }) { - const params = await props.params; - const page = source.getPage(params.slug); - if (!page) { - permanentRedirect("/docs/core"); - } + const params = await props.params; + const page = source.getPage(params.slug); + if (!page) { + permanentRedirect("/docs/core"); + } - const MDX = page.data.body; + const MDX = page.data.body; - return ( - - {page.data.title} - {page.data.description} - - , - p: ({ children }) => ( -

- {children} -

- ), - li: ({ children, id }) => ( -
  • - {children} -
  • - ), - APIPage: openapi.APIPage, - }} - /> -
    -
    - ); + return ( + + {page.data.title} + {page.data.description} + + , + p: ({ children }) => ( +

    + {children} +

    + ), + li: ({ children, id }) => ( +
  • + {children} +
  • + ), + APIPage: openapi.APIPage, + }} + /> +
    +
    + ); } export async function generateStaticParams() { - return source.generateParams(); + return source.generateParams(); } export async function generateMetadata(props: { - params: Promise<{ slug?: string[] }>; + params: Promise<{ slug?: string[] }>; }) { - const params = await props.params; - const page = source.getPage(params.slug); - if (!page) notFound(); + const params = await props.params; + const page = source.getPage(params.slug); + if (!page) notFound(); - return { - title: page.data.title, + return { + title: page.data.title, - description: page.data.description, - robots: "index,follow", - alternates: { - canonical: new URL(`${baseUrl}${page.url}`).toString(), - languages: { - en: `${baseUrl}/${page.url}`, - }, - }, - openGraph: { - title: page.data.title, - description: page.data.description, - url: new URL(`${baseUrl}`).toString(), - images: [ - { - url: new URL(`${baseUrl}/logo.png`).toString(), - width: 1200, - height: 630, - alt: page.data.title, - }, - ], - }, - twitter: { - card: "summary_large_image", - creator: "@getdokploy", - title: page.data.title, - description: page.data.description, - images: [ - { - url: new URL(`${baseUrl}/logo.png`).toString(), - width: 1200, - height: 630, - alt: page.data.title, - }, - ], - }, - applicationName: "Dokploy Docs", - keywords: [ - "dokploy", - "vps", - "open source", - "cloud", - "self hosting", - "free", - ], - icons: { - icon: "/icon.svg", - }, - }; + description: page.data.description, + robots: "index,follow", + alternates: { + canonical: new URL(`${baseUrl}${page.url}`).toString(), + languages: { + en: `${baseUrl}/${page.url}`, + }, + }, + openGraph: { + title: page.data.title, + description: page.data.description, + url: new URL(`${baseUrl}`).toString(), + images: [ + { + url: new URL(`${baseUrl}/logo.png`).toString(), + width: 1200, + height: 630, + alt: page.data.title, + }, + ], + }, + twitter: { + card: "summary_large_image", + creator: "@getdokploy", + title: page.data.title, + description: page.data.description, + images: [ + { + url: new URL(`${baseUrl}/logo.png`).toString(), + width: 1200, + height: 630, + alt: page.data.title, + }, + ], + }, + applicationName: "Dokploy Docs", + keywords: [ + "dokploy", + "vps", + "open source", + "cloud", + "self hosting", + "free", + ], + icons: { + icon: "/icon.svg", + }, + }; } diff --git a/apps/docs/app/docs/layout.tsx b/apps/docs/app/docs/layout.tsx index fb8ff11..322a090 100644 --- a/apps/docs/app/docs/layout.tsx +++ b/apps/docs/app/docs/layout.tsx @@ -5,18 +5,18 @@ import { DocsLayout } from "fumadocs-ui/layouts/docs"; import type { ReactNode } from "react"; export const metadata = createMetadata({ - title: { - template: "%s | Dokploy", - default: "Dokploy", - }, - description: "The Open Source Alternative to Vercel, Heroku, and Netlify", - metadataBase: new URL(baseUrl), + title: { + template: "%s | Dokploy", + default: "Dokploy", + }, + description: "The Open Source Alternative to Vercel, Heroku, and Netlify", + metadataBase: new URL(baseUrl), }); export default function Layout({ children }: { children: ReactNode }) { - return ( - - {children} - - ); + return ( + + {children} + + ); } diff --git a/apps/docs/app/layout.config.tsx b/apps/docs/app/layout.config.tsx index d04993b..b643745 100644 --- a/apps/docs/app/layout.config.tsx +++ b/apps/docs/app/layout.config.tsx @@ -1,11 +1,11 @@ import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared"; import { - Github, - GlobeIcon, - HeartIcon, - Rss, - LogIn, - UserPlus, + Github, + GlobeIcon, + HeartIcon, + Rss, + LogIn, + UserPlus, } from "lucide-react"; import Link from "next/link"; /** @@ -17,106 +17,106 @@ import Link from "next/link"; */ export const Logo = () => { - return ( - - + return ( + + - + - - - ); + + + ); }; export const baseOptions: BaseLayoutProps = { - nav: { - // title: "Dokploy", - children: ( - - - Dokploy - - ), - }, - links: [ - { - text: "Login", - url: "https://app.dokploy.com/", - active: "nested-url", - icon: , - }, - { - text: "Sign Up", - url: "https://app.dokploy.com/register", - active: "nested-url", - icon: , - }, - { - text: "Website", - url: "https://dokploy.com", - active: "nested-url", - icon: , - }, - { - text: "Discord", - url: "https://discord.com/invite/2tBnJ3jDJc", - active: "nested-url", - icon: ( - <> - - - - - ), - }, - { - text: "Support", - url: "https://opencollective.com/dokploy", - active: "nested-url", - icon: ( - <> - - - ), - }, - { - text: "Github", - url: "https://github.com/dokploy/dokploy", - active: "nested-url", - icon: ( - <> - - - ), - }, - { - text: "Blog", - url: "https://dokploy.com/blog", - active: "nested-url", - icon: ( - <> - - - ), - }, - ], + nav: { + // title: "Dokploy", + children: ( + + + Dokploy + + ), + }, + links: [ + { + text: "Login", + url: "https://app.dokploy.com/", + active: "nested-url", + icon: , + }, + { + text: "Sign Up", + url: "https://app.dokploy.com/register", + active: "nested-url", + icon: , + }, + { + text: "Website", + url: "https://dokploy.com", + active: "nested-url", + icon: , + }, + { + text: "Discord", + url: "https://discord.com/invite/2tBnJ3jDJc", + active: "nested-url", + icon: ( + <> + + + + + ), + }, + { + text: "Support", + url: "https://opencollective.com/dokploy", + active: "nested-url", + icon: ( + <> + + + ), + }, + { + text: "Github", + url: "https://github.com/dokploy/dokploy", + active: "nested-url", + icon: ( + <> + + + ), + }, + { + text: "Blog", + url: "https://dokploy.com/blog", + active: "nested-url", + icon: ( + <> + + + ), + }, + ], }; diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index 1c6adc0..d2bd648 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -4,19 +4,21 @@ import { Inter } from "next/font/google"; import type { ReactNode } from "react"; import { GoogleAnalytics } from "@next/third-parties/google"; const inter = Inter({ - subsets: ["latin"], + subsets: ["latin"], }); export default async function Layout({ - children, - ...rest -}: { children: ReactNode }) { - return ( - - - - {children} - - - ); + children, + ...rest +}: { + children: ReactNode; +}) { + return ( + + + + {children} + + + ); } diff --git a/apps/docs/app/robots.ts b/apps/docs/app/robots.ts index 3b00704..1c2ea94 100644 --- a/apps/docs/app/robots.ts +++ b/apps/docs/app/robots.ts @@ -1,11 +1,11 @@ import type { MetadataRoute } from "next"; export default function robots(): MetadataRoute.Robots { - return { - rules: { - userAgent: "*", - allow: "/", - }, - sitemap: "https://docs.dokploy.com/sitemap.xml", - }; + return { + rules: { + userAgent: "*", + allow: "/", + }, + sitemap: "https://docs.dokploy.com/sitemap.xml", + }; } diff --git a/apps/docs/app/sitemap.ts b/apps/docs/app/sitemap.ts index 39ca0aa..49a40f1 100644 --- a/apps/docs/app/sitemap.ts +++ b/apps/docs/app/sitemap.ts @@ -3,17 +3,17 @@ import { url } from "@/utils/metadata"; import type { MetadataRoute } from "next"; export default async function sitemap(): Promise { - return [ - ...(await Promise.all( - source.getPages().map(async (page) => { - const { lastModified } = page.data; - return { - url: url(page.url), - lastModified: lastModified ? new Date(lastModified) : undefined, - changeFrequency: "weekly", - priority: 0.5, - } as MetadataRoute.Sitemap[number]; - }), - )), - ]; + return [ + ...(await Promise.all( + source.getPages().map(async (page) => { + const { lastModified } = page.data; + return { + url: url(page.url), + lastModified: lastModified ? new Date(lastModified) : undefined, + changeFrequency: "weekly", + priority: 0.5, + } as MetadataRoute.Sitemap[number]; + }), + )), + ]; } diff --git a/apps/docs/lib/source.ts b/apps/docs/lib/source.ts index 5593e67..d588396 100644 --- a/apps/docs/lib/source.ts +++ b/apps/docs/lib/source.ts @@ -5,13 +5,13 @@ import { createOpenAPI } from "fumadocs-openapi/server"; import { attachFile } from "fumadocs-openapi/server"; export const source = loader({ - baseUrl: "/docs", - source: createMDXSource(docs, meta), - // pageTree: { - // attachFile, - // }, + baseUrl: "/docs", + source: createMDXSource(docs, meta), + // pageTree: { + // attachFile, + // }, }); export const openapi = createOpenAPI({ - // options + // options }); diff --git a/apps/docs/source.config.ts b/apps/docs/source.config.ts index 62ab25d..443d9d3 100644 --- a/apps/docs/source.config.ts +++ b/apps/docs/source.config.ts @@ -1,7 +1,7 @@ import { defineConfig, defineDocs } from "fumadocs-mdx/config"; export const { docs, meta } = defineDocs({ - dir: "content/docs", + dir: "content/docs", }); export default defineConfig(); diff --git a/apps/docs/utils/metadata.ts b/apps/docs/utils/metadata.ts index c68397d..8bc700d 100644 --- a/apps/docs/utils/metadata.ts +++ b/apps/docs/utils/metadata.ts @@ -1,30 +1,30 @@ import type { Metadata } from "next"; export const baseUrl = - process.env.NODE_ENV === "development" - ? "http://localhost:3000" - : "https://docs.dokploy.com"; + process.env.NODE_ENV === "development" + ? "http://localhost:3000" + : "https://docs.dokploy.com"; export const url = (path: string): string => new URL(path, baseUrl).toString(); export function createMetadata(override: Metadata): Metadata { - return { - ...override, - openGraph: { - title: override.title ?? undefined, - description: override.description ?? undefined, - url: "https://fumadocs.vercel.app", - images: "/og.png", - siteName: "Fumadocs", - ...override.openGraph, - }, - twitter: { - card: "summary_large_image", - creator: "@money_is_shark", - title: override.title ?? undefined, - description: override.description ?? undefined, - images: "/banner.png", - ...override.twitter, - }, - }; + return { + ...override, + openGraph: { + title: override.title ?? undefined, + description: override.description ?? undefined, + url: "https://fumadocs.vercel.app", + images: "/og.png", + siteName: "Fumadocs", + ...override.openGraph, + }, + twitter: { + card: "summary_large_image", + creator: "@money_is_shark", + title: override.title ?? undefined, + description: override.description ?? undefined, + images: "/banner.png", + ...override.twitter, + }, + }; } diff --git a/apps/website/README.md b/apps/website/README.md index 5a7f037..4d88448 100644 --- a/apps/website/README.md +++ b/apps/website/README.md @@ -19,18 +19,21 @@ 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="" -``` \ No newline at end of file +``` diff --git a/apps/website/app/api/contact/route.ts b/apps/website/app/api/contact/route.ts index 26469e1..1322171 100644 --- a/apps/website/app/api/contact/route.ts +++ b/apps/website/app/api/contact/route.ts @@ -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 ", + from: 'Dokploy Contact Form ', 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 ", + from: 'Dokploy Team ', 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 }, - ); + ) } } diff --git a/apps/website/app/api/github-stars/route.ts b/apps/website/app/api/github-stars/route.ts index 57c7a4d..9426ff5 100644 --- a/apps/website/app/api/github-stars/route.ts +++ b/apps/website/app/api/github-stars/route.ts @@ -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 }, - ); + ) } } - diff --git a/apps/website/app/api/og/route.ts b/apps/website/app/api/og/route.ts index 53cac07..2bbbddd 100644 --- a/apps/website/app/api/og/route.ts +++ b/apps/website/app/api/og/route.ts @@ -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 }) } } diff --git a/apps/website/app/blog/[slug]/components/CodeBlock.tsx b/apps/website/app/blog/[slug]/components/CodeBlock.tsx index da8be4e..ac8ea91 100644 --- a/apps/website/app/blog/[slug]/components/CodeBlock.tsx +++ b/apps/website/app/blog/[slug]/components/CodeBlock.tsx @@ -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(initial); - const [formattedCode, setFormattedCode] = useState(code); + const [nodes, setNodes] = useState(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 (
    -
    -
    -
    +
    +
    +
    - ); + ) } return (
    -
    +
    {nodes}
    - ); + ) } diff --git a/apps/website/app/blog/[slug]/components/Headings.tsx b/apps/website/app/blog/[slug]/components/Headings.tsx index cc6ac6d..7385214 100644 --- a/apps/website/app/blog/[slug]/components/Headings.tsx +++ b/apps/website/app/blog/[slug]/components/Headings.tsx @@ -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 ->; +> function LinkIcon() { return ( - ); + ) } 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 (

    {children}

    - ); + ) } 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 (

    {children}

    - ); + ) } 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 (

    {children}

    - ); + ) } diff --git a/apps/website/app/blog/[slug]/components/TableOfContents.tsx b/apps/website/app/blog/[slug]/components/TableOfContents.tsx index 3bbaac3..099d3db 100644 --- a/apps/website/app/blog/[slug]/components/TableOfContents.tsx +++ b/apps/website/app/blog/[slug]/components/TableOfContents.tsx @@ -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([]); - const [activeId, setActiveId] = useState(); + const [headings, setHeadings] = useState([]) + const [activeId, setActiveId] = useState() 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 ( - ); + ) } diff --git a/apps/website/app/blog/[slug]/components/ZoomableImage.tsx b/apps/website/app/blog/[slug]/components/ZoomableImage.tsx index 7b97960..cbcc66e 100644 --- a/apps/website/app/blog/[slug]/components/ZoomableImage.tsx +++ b/apps/website/app/blog/[slug]/components/ZoomableImage.tsx @@ -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 ( - {alt} + {alt} - ); + ) } diff --git a/apps/website/app/blog/[slug]/components/shared.ts b/apps/website/app/blog/[slug]/components/shared.ts index 4e6096b..a2daa08 100644 --- a/apps/website/app/blog/[slug]/components/shared.ts +++ b/apps/website/app/blog/[slug]/components/shared.ts @@ -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 } diff --git a/apps/website/app/blog/[slug]/page.tsx b/apps/website/app/blog/[slug]/page.tsx index 03e63cf..4bad59b 100644 --- a/apps/website/app/blog/[slug]/page.tsx +++ b/apps/website/app/blog/[slug]/page.tsx @@ -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 { - 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( /]*type="application\/ld\+json"[^>]*>[\s\S]*?<\/script>/gi, - "", + '', ) - .replace(/]*>[\s\S]*?<\/script>/gi, "") - .replace(/]*>[\s\S]*?<\/style>/gi, "") - .replace(/]*>/gi, "") - .replace(/]*>/gi, ""); + .replace(/]*>[\s\S]*?<\/script>/gi, '') + .replace(/]*>[\s\S]*?<\/style>/gi, '') + .replace(/]*>/gi, '') + .replace(/]*>/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 = { h1: H1, @@ -128,7 +128,7 @@ export default async function BlogPostPage({ params }: Props) { h3: H3, p: ({ node, children, ...props }) => (

    {children} @@ -137,7 +137,7 @@ export default async function BlogPostPage({ params }: Props) { a: ({ node, href, ...props }) => ( (