From ac9ad7287555f1752a27dd74b8ec3bf071bf621b Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Fri, 14 Aug 2026 09:24:54 -0600 Subject: [PATCH] feat: interactive feature showcase on the signup panel Replaces the static quote in the onboarding layout's left panel with an auto-rotating carousel of product highlights (Deploy / Manage / Scale, 3 slides x 4 items), shown on the cloud signup page only. - OnboardingLayout gets an optional leftPanel override, default quote untouched everywhere else (login, invitation, password reset). - Auto-advances every 6s, pauses on hover/focus, respects prefers-reduced-motion, manual dot + arrow navigation. - Copy pulled from the website's existing first-features.tsx section to stay consistent with approved marketing copy. --- .../components/layouts/onboarding-layout.tsx | 12 +- .../proprietary/auth/signup-showcase.tsx | 204 ++++++++++++++++++ apps/dokploy/pages/register.tsx | 8 +- 3 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 apps/dokploy/components/proprietary/auth/signup-showcase.tsx diff --git a/apps/dokploy/components/layouts/onboarding-layout.tsx b/apps/dokploy/components/layouts/onboarding-layout.tsx index 297a9d8a7..ed46c1930 100644 --- a/apps/dokploy/components/layouts/onboarding-layout.tsx +++ b/apps/dokploy/components/layouts/onboarding-layout.tsx @@ -8,8 +8,10 @@ import { Button } from "../ui/button"; interface Props { children: React.ReactNode; + /** Overrides the default quote in the left panel (desktop only). */ + leftPanel?: React.ReactNode; } -export const OnboardingLayout = ({ children }: Props) => { +export const OnboardingLayout = ({ children, leftPanel }: Props) => { const { config: whitelabeling } = useWhitelabelingPublic(); const appName = whitelabeling?.appName || "Dokploy"; const appDescription = @@ -30,9 +32,11 @@ export const OnboardingLayout = ({ children }: Props) => { {appName}
-
-

{appDescription}

-
+ {leftPanel ?? ( +
+

{appDescription}

+
+ )}
diff --git a/apps/dokploy/components/proprietary/auth/signup-showcase.tsx b/apps/dokploy/components/proprietary/auth/signup-showcase.tsx new file mode 100644 index 000000000..e67edf608 --- /dev/null +++ b/apps/dokploy/components/proprietary/auth/signup-showcase.tsx @@ -0,0 +1,204 @@ +"use client"; + +import { + Activity, + ArrowRight, + Bot, + Boxes, + Cloud, + Database, + Layers, + LayoutTemplate, + Rocket, + Shield, + Terminal, + Unlock, + Users, +} from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { cn } from "@/lib/utils"; + +interface ShowcaseItem { + icon: typeof Users; + title: string; + description: string; +} + +const SLIDES: ShowcaseItem[][] = [ + [ + { + icon: Users, + title: "Team access", + description: + "Anyone in your team can deploy simply and securely in a safe, governed environment.", + }, + { + icon: Layers, + title: "Open source", + description: + "Deploy for free with the open-source alternative to Netlify, Vercel, and Heroku.", + }, + { + icon: Bot, + title: "AI sandbox", + description: + "Unleash the power of AI and test AI-generated code in a sandbox before deploying to a live URL.", + }, + { + icon: Shield, + title: "Enterprise ready", + description: + "Scale when you're ready with granular RBAC, SSO, audit logs, rollback and multi-tenancy.", + }, + ], + [ + { + icon: Rocket, + title: "Any stack", + description: + "Deploy any application using Nixpacks, Heroku Buildpacks, or your own Dockerfile.", + }, + { + icon: Boxes, + title: "Docker Compose", + description: + "Deploy complex applications natively with full Docker Compose integration.", + }, + { + icon: Cloud, + title: "Multi-server", + description: + "Effortlessly deploy your applications on remote servers, with zero configuration hassle.", + }, + { + icon: LayoutTemplate, + title: "Ready templates", + description: + "Get started quickly with pre-configured templates for Supabase, Cal.com, PocketBase, and more.", + }, + ], + [ + { + icon: Database, + title: "Managed databases", + description: + "Manage and back up MySQL, PostgreSQL, MongoDB, MariaDB, and Redis directly from Dokploy.", + }, + { + icon: Terminal, + title: "API & CLI", + description: "Full API and CLI access to fit any custom workflow.", + }, + { + icon: Activity, + title: "Live monitoring", + description: + "Monitor CPU, memory, and network usage in real time across every deployment.", + }, + { + icon: Unlock, + title: "No lock-in", + description: + "Modify, scale, and customize Dokploy however your project needs.", + }, + ], +]; + +const AUTO_ADVANCE_MS = 6000; + +export const SignupShowcase = () => { + const [active, setActive] = useState(0); + const [paused, setPaused] = useState(false); + const [reduceMotion, setReduceMotion] = useState(false); + + useEffect(() => { + const query = window.matchMedia("(prefers-reduced-motion: reduce)"); + setReduceMotion(query.matches); + const listener = (e: MediaQueryListEvent) => setReduceMotion(e.matches); + query.addEventListener("change", listener); + return () => query.removeEventListener("change", listener); + }, []); + + const timerRef = useRef | null>(null); + + useEffect(() => { + if (paused || reduceMotion) { + return; + } + timerRef.current = setInterval(() => { + setActive((prev) => (prev + 1) % SLIDES.length); + }, AUTO_ADVANCE_MS); + return () => { + if (timerRef.current) { + clearInterval(timerRef.current); + } + }; + }, [paused, reduceMotion]); + + const goTo = (index: number) => { + setActive(((index % SLIDES.length) + SLIDES.length) % SLIDES.length); + }; + + return ( +
setPaused(true)} + onMouseLeave={() => setPaused(false)} + onFocus={() => setPaused(true)} + onBlur={() => setPaused(false)} + > +
+ {SLIDES[active]?.map((item) => ( +
+
+ +
+
+

{item.title}

+

+ {item.description} +

+
+
+ ))} +
+ +
+ {SLIDES.map((slide, index) => ( +
+ + +
+ ); +}; diff --git a/apps/dokploy/pages/register.tsx b/apps/dokploy/pages/register.tsx index f18747773..5510147d5 100644 --- a/apps/dokploy/pages/register.tsx +++ b/apps/dokploy/pages/register.tsx @@ -11,6 +11,7 @@ import { z } from "zod"; import { OnboardingLayout } from "@/components/layouts/onboarding-layout"; import { SignInWithGithub } from "@/components/proprietary/auth/sign-in-with-github"; import { SignInWithGoogle } from "@/components/proprietary/auth/sign-in-with-google"; +import { SignupShowcase } from "@/components/proprietary/auth/signup-showcase"; import { AlertBlock } from "@/components/shared/alert-block"; import { Logo } from "@/components/shared/logo"; import { Button } from "@/components/ui/button"; @@ -294,7 +295,12 @@ const Register = ({ isCloud }: Props) => { export default Register; Register.getLayout = (page: ReactElement) => { - return {page}; + const isCloud = (page.props as Props).isCloud; + return ( + : undefined}> + {page} + + ); }; export async function getServerSideProps(context: GetServerSidePropsContext) { if (IS_CLOUD) {