├── .env.example
├── vercel.json
├── public
├── favicon.ico
├── og-image.png
├── favicon-16x16.png
├── favicon-32x32.png
├── apple-touch-icon.png
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── robots.txt
└── manifest.json
├── .gitignore
├── src
├── lib
│ ├── utils.ts
│ └── meta-tags.ts
├── components
│ ├── ui
│ │ ├── skeleton.tsx
│ │ ├── label.tsx
│ │ ├── textarea.tsx
│ │ ├── collapsible.tsx
│ │ ├── separator.tsx
│ │ ├── input.tsx
│ │ ├── checkbox.tsx
│ │ ├── empty-state.tsx
│ │ ├── toggle.tsx
│ │ ├── progress.tsx
│ │ ├── alert.tsx
│ │ ├── tooltip.tsx
│ │ ├── card.tsx
│ │ ├── button.tsx
│ │ ├── tabs.tsx
│ │ ├── toast.tsx
│ │ ├── form.tsx
│ │ ├── dialog.tsx
│ │ ├── sheet.tsx
│ │ └── select.tsx
│ ├── Footer.tsx
│ ├── ThemeProvider.tsx
│ ├── MetaTags.tsx
│ ├── templates
│ │ └── TemplateCard.tsx
│ ├── Header.tsx
│ ├── ConversionDialog.tsx
│ ├── CodeEditor.tsx
│ ├── SidebarUI.tsx
│ └── compose-builder
│ │ ├── ServiceListSidebar.tsx
│ │ ├── VolumeForm.tsx
│ │ └── NetworkForm.tsx
├── reportWebVitals.ts
├── hooks
│ ├── use-mobile.ts
│ ├── use-search-params.ts
│ ├── useEditorSize.ts
│ ├── useSelectionState.ts
│ ├── useYamlValidation.ts
│ ├── useConversionDialog.ts
│ ├── useVpnConfig.ts
│ ├── useNetworkVolumeManager.ts
│ └── useTemplateStore.ts
├── types
│ ├── vpn.ts
│ ├── vpn-configs.ts
│ └── compose.ts
├── main.tsx
├── routes
│ ├── __root.tsx
│ └── $.tsx
├── utils
│ ├── clipboard.ts
│ ├── default-configs.ts
│ ├── env-generator.ts
│ ├── validation.ts
│ ├── converters.ts
│ ├── yaml-comments.ts
│ └── vpn-generator.ts
├── routeTree.gen.ts
└── styles.css
├── .cta.json
├── .dockerignore
├── nginx.conf
├── components.json
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ └── bug_report.yml
└── workflows
│ └── docker-build-push.yml
├── Dockerfile
├── vite.config.ts
├── tsconfig.json
├── docker-compose.yml
├── index.html
└── package.json
/.env.example:
--------------------------------------------------------------------------------
1 | VITE_ENV=prod
2 | VITE_SELF_HOSTED=true
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [{ "source": "/(.*)", "destination": "/" }]
3 | }
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hhftechnology/Dock-Dploy/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/og-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hhftechnology/Dock-Dploy/HEAD/public/og-image.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hhftechnology/Dock-Dploy/HEAD/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hhftechnology/Dock-Dploy/HEAD/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hhftechnology/Dock-Dploy/HEAD/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hhftechnology/Dock-Dploy/HEAD/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hhftechnology/Dock-Dploy/HEAD/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
6 | count.txt
7 | .env
8 | .nitro
9 | .tanstack
10 | /cloudflare/
11 | /.wrangler/
12 | /docker/docker-compose.yml
13 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from 'clsx'
2 | import { twMerge } from 'tailwind-merge'
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Allow: /
4 |
5 | # Disallow API and backend routes
6 | Disallow: /api/
7 | Disallow: /backend/
8 | Disallow: /src/
9 |
10 | # Allow main pages
11 | Allow: /
12 | Allow: /docker/compose-builder
--------------------------------------------------------------------------------
/.cta.json:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "Dock-Dploy",
3 | "mode": "file-router",
4 | "typescript": true,
5 | "tailwind": true,
6 | "packageManager": "npm",
7 | "git": true,
8 | "version": 1,
9 | "framework": "react-cra",
10 | "chosenAddOns": ["biome", "shadcn"]
11 | }
12 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | dist-ssr
4 | .DS_Store
5 | *.local
6 | .env
7 | .git
8 | .gitignore
9 | .github
10 | README.md
11 | .vscode
12 | .idea
13 | *.log
14 | npm-debug.log*
15 | yarn-debug.log*
16 | yarn-error.log*
17 | .tanstack
18 | count.txt
19 | .nitro
20 | /cloudflare/
21 | /.wrangler/
22 | /docker/docker-compose.yml
23 |
24 |
--------------------------------------------------------------------------------
/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 3000;
3 | server_name localhost;
4 |
5 | root /usr/share/nginx/html;
6 | index index.html;
7 |
8 | location / {
9 | try_files $uri $uri/ /index.html;
10 | }
11 |
12 | location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
13 | expires 1y;
14 | add_header Cache-Control "public, immutable";
15 | }
16 | }
--------------------------------------------------------------------------------
/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import {cn} from "../../lib/utils"
2 |
3 | function Skeleton({className, ...props}: React.ComponentProps<"div">) {
4 | return (
5 |
10 | )
11 | }
12 |
13 | export {Skeleton}
14 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Dock-Dploy",
3 | "name": "Build Docker Compose Files",
4 | "icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],
5 | "start_url": ".",
6 | "display": "standalone",
7 | "theme_color": "#000000",
8 | "background_color": "#ffffff"
9 | }
10 |
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | const reportWebVitals = (onPerfEntry?: () => void) => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({onCLS, onINP, onFCP, onLCP, onTTFB}) => {
4 | onCLS(onPerfEntry)
5 | onINP(onPerfEntry)
6 | onFCP(onPerfEntry)
7 | onLCP(onPerfEntry)
8 | onTTFB(onPerfEntry)
9 | })
10 | }
11 | }
12 |
13 | export default reportWebVitals
14 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "src/styles.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: New Feature request
4 | url: https://github.com/hhftechnology/Dock-Dploy/discussions/new?category=request-feature
5 | about: For feature/script requests, please use the Discussions section.
6 | - name: 🤔 Questions and Help
7 | url: https://forum.hhf.technology/c/help/85
8 | about: For suggestions or questions, please use the Our forums.
9 | - name: Discord
10 | url: https://discord.gg/HDCt9MjyMJ
11 | about: Join our Discord server to chat with other users in the hhf community.
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Build stage
2 | FROM node:18-alpine AS builder
3 |
4 | WORKDIR /app
5 |
6 | # Copy package files
7 | COPY package*.json ./
8 |
9 | # Install dependencies
10 | RUN npm ci
11 |
12 | # Copy source code
13 | COPY . .
14 |
15 | # Build the application
16 | RUN npm run build
17 |
18 | # Production stage
19 | FROM nginx:alpine
20 |
21 | # Copy nginx configuration
22 | COPY nginx.conf /etc/nginx/conf.d/default.conf
23 |
24 | # Copy built files from builder stage
25 | COPY --from=builder /app/dist /usr/share/nginx/html
26 |
27 | # Expose port 3000
28 | EXPOSE 3000
29 |
30 | # Start nginx
31 | CMD ["nginx", "-g", "daemon off;"]
32 |
33 |
--------------------------------------------------------------------------------
/src/hooks/use-mobile.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 |
4 | import { cn } from "../../lib/utils"
5 |
6 | function Label({
7 | className,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
19 | )
20 | }
21 |
22 | export { Label }
23 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import viteReact from "@vitejs/plugin-react";
3 | import { resolve } from "node:path";
4 | import { fileURLToPath } from "node:url";
5 |
6 | import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
7 | import tailwindcss from "@tailwindcss/vite";
8 |
9 | // Fix __dirname for ESM
10 | const __filename = fileURLToPath(import.meta.url);
11 | const __dirname = resolve(__filename, "..");
12 |
13 | export default defineConfig({
14 | plugins: [
15 | TanStackRouterVite({ autoCodeSplitting: true }),
16 | viteReact(),
17 | tailwindcss(),
18 | ],
19 | resolve: {
20 | alias: {
21 | "@": resolve(__dirname, "./src"),
22 | },
23 | },
24 | });
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "compilerOptions": {
4 | "target": "ES2022",
5 | "jsx": "react-jsx",
6 | "module": "ESNext",
7 | "lib": ["ES2022", "DOM", "DOM.Iterable"],
8 | "types": ["vite/client"],
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "verbatimModuleSyntax": true,
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "skipLibCheck": true,
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "noUncheckedSideEffectImports": true,
23 | "baseUrl": ".",
24 | "paths": {
25 | "@/*": ["src/*"]
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "../../lib/utils"
4 |
5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | export { Textarea }
19 |
--------------------------------------------------------------------------------
/src/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "./ui/button";
2 | import { Separator } from "./ui/separator";
3 |
4 | export function Footer() {
5 |
6 | return (
7 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/types/vpn.ts:
--------------------------------------------------------------------------------
1 | // VPN provider abstraction types
2 |
3 | export interface VPNProviderConfig {
4 | enabled: boolean;
5 | type: string;
6 | servicesUsingVpn: string[];
7 | }
8 |
9 | export interface VPNServiceGenerator {
10 | generateService(config: VPNProviderConfig): any;
11 | generateVolumes(config: VPNProviderConfig): any[];
12 | generateNetworks(config: VPNProviderConfig): any[];
13 | getServiceName(): string;
14 | usesNetworkMode(): boolean;
15 | supportsHealthCheck(): boolean;
16 | }
17 |
18 | export type VPNProviderType =
19 | | "tailscale"
20 | | "newt"
21 | | "cloudflared"
22 | | "wireguard"
23 | | "zerotier"
24 | | "netbird";
25 |
26 | export interface VPNProviderRegistry {
27 | register(type: VPNProviderType, provider: VPNServiceGenerator): void;
28 | get(type: VPNProviderType): VPNServiceGenerator | undefined;
29 | getAll(): VPNProviderType[];
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/src/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
2 |
3 | function Collapsible({
4 | ...props
5 | }: React.ComponentProps) {
6 | return
7 | }
8 |
9 | function CollapsibleTrigger({
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
17 | )
18 | }
19 |
20 | function CollapsibleContent({
21 | ...props
22 | }: React.ComponentProps) {
23 | return (
24 |
28 | )
29 | }
30 |
31 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
32 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import {StrictMode} from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import {RouterProvider, createRouter} from '@tanstack/react-router'
4 |
5 | import {routeTree} from './routeTree.gen.ts'
6 |
7 | import './styles.css'
8 | import reportWebVitals from './reportWebVitals.ts'
9 |
10 | const router = createRouter({
11 | routeTree,
12 | context: {},
13 | defaultPreload: 'intent',
14 | scrollRestoration: true,
15 | defaultStructuralSharing: true,
16 | defaultPreloadStaleTime: 0,
17 | })
18 |
19 | declare module '@tanstack/react-router' {
20 | interface Register {
21 | router: typeof router
22 | }
23 | }
24 |
25 | const rootElement = document.getElementById('app')
26 | if (rootElement && !rootElement.innerHTML) {
27 | const root = ReactDOM.createRoot(rootElement)
28 | root.render(
29 |
30 |
31 | ,
32 | )
33 | }
34 |
35 | reportWebVitals()
36 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import {cn} from "../../lib/utils"
7 |
8 | function Separator({
9 | className,
10 | orientation = "horizontal",
11 | decorative = true,
12 | ...props
13 | }: React.ComponentProps) {
14 | return (
15 |
25 | )
26 | }
27 |
28 | export {Separator}
29 |
--------------------------------------------------------------------------------
/src/hooks/use-search-params.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useCallback } from 'react';
2 |
3 | export function useSearchParams() {
4 | const [searchParams, setSearchParamsState] = useState(
5 | new URLSearchParams(window.location.search)
6 | );
7 |
8 | useEffect(() => {
9 | const handlePopState = () => {
10 | setSearchParamsState(new URLSearchParams(window.location.search));
11 | };
12 | window.addEventListener('popstate', handlePopState);
13 | return () => window.removeEventListener('popstate', handlePopState);
14 | }, []);
15 |
16 | const setSearchParams = useCallback((newParams: Record | URLSearchParams) => {
17 | const nextParams = new URLSearchParams(
18 | newParams instanceof URLSearchParams ? newParams : newParams
19 | );
20 | const newUrl = `${window.location.pathname}?${nextParams.toString()}`;
21 | window.history.pushState({}, '', newUrl);
22 | setSearchParamsState(nextParams);
23 | }, []);
24 |
25 | return [searchParams, setSearchParams] as const;
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import {cn} from "../../lib/utils"
4 |
5 | function Input({className, type, ...props}: React.ComponentProps<"input">) {
6 | return (
7 |
18 | )
19 | }
20 |
21 | export {Input}
22 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | dock-dploy:
5 | # Use the image from Docker Hub (replace YOUR_USERNAME with your Docker Hub username)
6 | # image: ${DOCKERHUB_USERNAME:-your-username}/dock-dploy:latest
7 | # Alternative: Use image from GitHub Container Registry
8 | # image: ghcr.io/${GITHUB_REPOSITORY:-your-username/dock-dploy}:latest
9 |
10 | # Uncomment below to build from local Dockerfile instead
11 | build:
12 | context: .
13 | dockerfile: Dockerfile
14 |
15 | container_name: dock-dploy
16 | restart: unless-stopped
17 | ports:
18 | - "3000:3000"
19 | environment:
20 | - NODE_ENV=production
21 | # Optional: Add healthcheck
22 | healthcheck:
23 | test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/"]
24 | interval: 30s
25 | timeout: 10s
26 | retries: 3
27 | start_period: 40s
28 | # Optional: Resource limits
29 | deploy:
30 | resources:
31 | limits:
32 | cpus: '1'
33 | memory: 512M
34 | reservations:
35 | cpus: '0.5'
36 | memory: 256M
37 |
38 |
--------------------------------------------------------------------------------
/src/routes/__root.tsx:
--------------------------------------------------------------------------------
1 | import {Outlet, createRootRoute, useRouterState} from '@tanstack/react-router'
2 | import {ThemeProvider} from "../components/ThemeProvider";
3 | import {Header} from "../components/Header";
4 | import {Footer} from "../components/Footer";
5 | import {MetaTags} from "../components/MetaTags";
6 | import {ToastProvider} from "../components/ui/toast";
7 | import {TooltipProvider} from "../components/ui/tooltip";
8 |
9 | export const Route = createRootRoute({
10 | component: RootComponent,
11 | })
12 |
13 | function RootComponent() {
14 | const router = useRouterState();
15 | const isIndexPage = router.location.pathname === '/';
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {isIndexPage && }
28 |
29 |
30 |
31 |
32 | );
33 | }
--------------------------------------------------------------------------------
/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
3 | import { Check } from "lucide-react"
4 | import { cn } from "../../lib/utils"
5 |
6 | const Checkbox = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
22 |
25 |
26 |
27 |
28 | ))
29 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
30 |
31 | export { Checkbox }
32 |
33 |
--------------------------------------------------------------------------------
/src/components/ui/empty-state.tsx:
--------------------------------------------------------------------------------
1 | import type { LucideIcon } from "lucide-react"
2 | import { cn } from "../../lib/utils"
3 | import { Button } from "./button"
4 |
5 | export interface EmptyStateProps {
6 | icon?: LucideIcon
7 | title: string
8 | description?: string
9 | action?: {
10 | label: string
11 | onClick: () => void
12 | }
13 | className?: string
14 | }
15 |
16 | export function EmptyState({
17 | icon: Icon,
18 | title,
19 | description,
20 | action,
21 | className,
22 | }: EmptyStateProps) {
23 | return (
24 |
31 | {Icon && (
32 |
33 |
34 |
35 | )}
36 |
{title}
37 | {description && (
38 |
39 | {description}
40 |
41 | )}
42 | {action && (
43 |
46 | )}
47 |
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/src/hooks/useEditorSize.ts:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect, useRef, useState } from "react";
2 |
3 | export interface EditorSize {
4 | width: number;
5 | height: number;
6 | }
7 |
8 | /**
9 | * Hook to manage code editor size with ResizeObserver
10 | * @returns Object containing codeFileRef and editorSize
11 | */
12 | export function useEditorSize() {
13 | const codeFileRef = useRef(null);
14 | const [editorSize, setEditorSize] = useState({ width: 0, height: 0 });
15 |
16 | useLayoutEffect(() => {
17 | if (!codeFileRef.current) return;
18 |
19 | const handleResize = () => {
20 | const rect = codeFileRef.current?.getBoundingClientRect();
21 | if (rect) {
22 | // Ensure minimum dimensions for small screens
23 | setEditorSize({
24 | width: Math.max(rect.width, 300),
25 | height: Math.max(rect.height, 200),
26 | });
27 | }
28 | };
29 |
30 | handleResize();
31 | const ro = new ResizeObserver(handleResize);
32 | ro.observe(codeFileRef.current);
33 |
34 | // Also listen to window resize for better responsiveness
35 | window.addEventListener("resize", handleResize);
36 |
37 | return () => {
38 | ro.disconnect();
39 | window.removeEventListener("resize", handleResize);
40 | };
41 | }, []);
42 |
43 | return {
44 | codeFileRef,
45 | editorSize,
46 | };
47 | }
48 |
--------------------------------------------------------------------------------
/src/utils/clipboard.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copy text to clipboard
3 | * @param text Text to copy to clipboard
4 | * @returns Promise that resolves when text is copied
5 | */
6 | export async function copyToClipboard(text: string): Promise {
7 | await navigator.clipboard.writeText(text);
8 | }
9 |
10 | /**
11 | * Download text as a file
12 | * @param content File content
13 | * @param filename Name of the file to download
14 | * @param mimeType MIME type of the file (default: text/plain)
15 | */
16 | export function downloadFile(
17 | content: string,
18 | filename: string,
19 | mimeType = "text/plain"
20 | ): void {
21 | const blob = new Blob([content], { type: mimeType });
22 | const url = URL.createObjectURL(blob);
23 | const a = document.createElement("a");
24 | a.href = url;
25 | a.download = filename;
26 | document.body.appendChild(a);
27 | a.click();
28 | document.body.removeChild(a);
29 | URL.revokeObjectURL(url);
30 | }
31 |
32 | /**
33 | * Download multiple files as separate downloads
34 | * @param files Array of file objects with filename and content
35 | */
36 | export function downloadMultipleFiles(
37 | files: Array<{ filename: string; content: string; mimeType?: string }>
38 | ): void {
39 | files.forEach((file, index) => {
40 | // Stagger downloads slightly to avoid browser blocking
41 | setTimeout(() => {
42 | downloadFile(file.content, file.filename, file.mimeType);
43 | }, index * 100);
44 | });
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TogglePrimitive from "@radix-ui/react-toggle"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "../../lib/utils"
6 |
7 | const toggleVariants = cva(
8 | "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-transparent",
13 | outline:
14 | "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
15 | },
16 | size: {
17 | default: "h-9 px-2 min-w-9",
18 | sm: "h-8 px-1.5 min-w-8",
19 | lg: "h-10 px-2.5 min-w-10",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | size: "default",
25 | },
26 | }
27 | )
28 |
29 | function Toggle({
30 | className,
31 | variant,
32 | size,
33 | ...props
34 | }: React.ComponentProps &
35 | VariantProps) {
36 | return (
37 |
42 | )
43 | }
44 |
45 | export { Toggle, toggleVariants }
46 |
--------------------------------------------------------------------------------
/src/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cn } from "../../lib/utils"
3 |
4 | export interface ProgressProps extends React.HTMLAttributes {
5 | value?: number
6 | max?: number
7 | showLabel?: boolean
8 | size?: "sm" | "md" | "lg"
9 | variant?: "default" | "success" | "warning" | "error"
10 | }
11 |
12 | const sizeStyles = {
13 | sm: "h-1",
14 | md: "h-2",
15 | lg: "h-3",
16 | }
17 |
18 | const variantStyles = {
19 | default: "bg-primary",
20 | success: "bg-green-500",
21 | warning: "bg-yellow-500",
22 | error: "bg-red-500",
23 | }
24 |
25 | const Progress = React.forwardRef(
26 | ({ className, value = 0, max = 100, showLabel = false, size = "md", variant = "default", ...props }, ref) => {
27 | const percentage = Math.min(Math.max((value / max) * 100, 0), 100)
28 |
29 | return (
30 |
31 |
52 | {showLabel && (
53 |
54 | {Math.round(percentage)}%
55 |
56 | )}
57 |
58 | )
59 | }
60 | )
61 |
62 | Progress.displayName = "Progress"
63 |
64 | export { Progress }
65 |
--------------------------------------------------------------------------------
/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "../../lib/utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-card text-card-foreground",
12 | destructive:
13 | "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | function Alert({
23 | className,
24 | variant,
25 | ...props
26 | }: React.ComponentProps<"div"> & VariantProps) {
27 | return (
28 |
34 | )
35 | }
36 |
37 | function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38 | return (
39 |
47 | )
48 | }
49 |
50 | function AlertDescription({
51 | className,
52 | ...props
53 | }: React.ComponentProps<"div">) {
54 | return (
55 |
63 | )
64 | }
65 |
66 | export { Alert, AlertTitle, AlertDescription }
67 |
--------------------------------------------------------------------------------
/src/components/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useEffect, useState } from "react";
2 |
3 | type Theme = "dark" | "light" | "system";
4 |
5 | type ThemeProviderProps = {
6 | children: React.ReactNode;
7 | defaultTheme?: Theme;
8 | storageKey?: string;
9 | };
10 |
11 | type ThemeProviderState = {
12 | theme: Theme;
13 | setTheme: (theme: Theme) => void;
14 | };
15 |
16 | const initialState: ThemeProviderState = {
17 | theme: "system",
18 | setTheme: () => null,
19 | };
20 |
21 | const ThemeProviderContext = createContext(initialState);
22 |
23 | export function ThemeProvider({
24 | children,
25 | defaultTheme = "system",
26 | storageKey = "vite-ui-theme",
27 | ...props
28 | }: ThemeProviderProps) {
29 | const [theme, setTheme] = useState(
30 | () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
31 | );
32 |
33 | useEffect(() => {
34 | const root = window.document.documentElement;
35 |
36 | root.classList.remove("light", "dark");
37 |
38 | if (theme === "system") {
39 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
40 | .matches
41 | ? "dark"
42 | : "light";
43 |
44 | root.classList.add(systemTheme);
45 | return;
46 | }
47 |
48 | root.classList.add(theme);
49 | }, [theme]);
50 |
51 | const value = {
52 | theme,
53 | setTheme: (theme: Theme) => {
54 | localStorage.setItem(storageKey, theme);
55 | setTheme(theme);
56 | },
57 | };
58 |
59 | return (
60 |
61 | {children}
62 |
63 | );
64 | }
65 |
66 | export const useTheme = () => {
67 | const context = useContext(ThemeProviderContext);
68 |
69 | if (context === undefined)
70 | throw new Error("useTheme must be used within a ThemeProvider");
71 |
72 | return context;
73 | };
74 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Dock-Dploy - Build Docker Compose Files Without the Hassle
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/types/vpn-configs.ts:
--------------------------------------------------------------------------------
1 | // VPN Provider Configuration Types
2 |
3 | export interface TailscaleConfig {
4 | authKey: string;
5 | hostname: string;
6 | acceptDns: boolean;
7 | authOnce: boolean;
8 | userspace: boolean;
9 | exitNode: string;
10 | exitNodeAllowLan: boolean;
11 | enableServe: boolean;
12 | serveConfig: string; // JSON string
13 | certDomain: string;
14 | serveTargetService: string;
15 | serveExternalPort: string;
16 | serveInternalPort: string;
17 | servePath: string;
18 | serveProtocol: "HTTPS" | "HTTP";
19 | serveInsideProtocol: "http" | "https" | "https+insecure";
20 | containerName: string;
21 | enableHealthCheck: boolean;
22 | healthCheckEndpoint: string;
23 | localAddrPort: string;
24 | dns: string[];
25 | configPath: string;
26 | stateDir: string;
27 | tmpfsEnabled: boolean;
28 | tmpfsPath: string;
29 | capAdd: string[];
30 | serveConfigPath: string;
31 | }
32 |
33 | export interface NewtConfig {
34 | endpoint: string;
35 | newtId: string;
36 | newtSecret: string;
37 | networkName: string;
38 | }
39 |
40 | export interface CloudflaredConfig {
41 | tunnelToken: string;
42 | noAutoupdate: boolean;
43 | }
44 |
45 | export interface WireguardConfig {
46 | configPath: string;
47 | interfaceName: string;
48 | }
49 |
50 | export interface ZerotierConfig {
51 | networkId: string;
52 | identityPath: string;
53 | }
54 |
55 | export interface NetbirdConfig {
56 | setupKey: string;
57 | managementUrl: string;
58 | }
59 |
60 | export interface VPNConfig {
61 | enabled: boolean;
62 | type:
63 | | "tailscale"
64 | | "newt"
65 | | "cloudflared"
66 | | "wireguard"
67 | | "zerotier"
68 | | "netbird"
69 | | null;
70 | tailscale?: TailscaleConfig;
71 | newt?: NewtConfig;
72 | cloudflared?: CloudflaredConfig;
73 | wireguard?: WireguardConfig;
74 | zerotier?: ZerotierConfig;
75 | netbird?: NetbirdConfig;
76 | servicesUsingVpn: string[]; // Service names that should use VPN
77 | networks?: string[]; // Networks the VPN service should attach to
78 | }
79 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Dock-Dploy",
3 | "private": true,
4 | "type": "module",
5 | "scripts": {
6 | "dev": "vite --port 3000",
7 | "start": "vite --port 3000",
8 | "build": "vite build && tsc",
9 | "serve": "vite preview",
10 | "test": "vitest run"
11 | },
12 | "dependencies": {
13 | "@codemirror/lang-yaml": "^6.1.2",
14 | "@hookform/resolvers": "^5.2.2",
15 | "@radix-ui/react-checkbox": "^1.3.3",
16 | "@radix-ui/react-collapsible": "^1.1.12",
17 | "@radix-ui/react-dialog": "^1.1.15",
18 | "@radix-ui/react-dropdown-menu": "^2.1.16",
19 | "@radix-ui/react-label": "^2.1.8",
20 | "@radix-ui/react-select": "^2.2.6",
21 | "@radix-ui/react-separator": "^1.1.8",
22 | "@radix-ui/react-slot": "^1.2.4",
23 | "@radix-ui/react-toggle": "^1.1.10",
24 | "@radix-ui/react-tooltip": "^1.2.8",
25 | "@tailwindcss/vite": "^4.0.6",
26 | "@tanstack/react-router": "^1.139.14",
27 | "@tanstack/react-router-devtools": "^1.139.14",
28 | "@tanstack/router-plugin": "^1.139.14",
29 | "@uiw/codemirror-extensions-hyper-link": "^4.25.3",
30 | "@uiw/codemirror-theme-monokai-dimmed": "^4.25.3",
31 | "@uiw/react-codemirror": "^4.25.3",
32 | "axios": "^1.10.0",
33 | "class-variance-authority": "^0.7.1",
34 | "clsx": "^2.1.1",
35 | "js-yaml": "^4.1.0",
36 | "lucide-react": "^0.556.0",
37 | "react": "^19.2.1",
38 | "react-dom": "^19.2.1",
39 | "react-hook-form": "^7.68.0",
40 | "tailwind-merge": "^3.0.2",
41 | "tailwindcss": "^4.0.6",
42 | "tailwindcss-animate": "^1.0.7",
43 | "zod": "^4.0.2"
44 | },
45 | "devDependencies": {
46 | "@biomejs/biome": "1.9.4",
47 | "@testing-library/dom": "^10.4.0",
48 | "@testing-library/react": "^16.3.0",
49 | "@types/js-yaml": "^4.0.9",
50 | "@types/node": "^24.0.10",
51 | "@types/react": "^19.2.7",
52 | "@types/react-dom": "^19.2.3",
53 | "@vitejs/plugin-react": "^5.1.1",
54 | "jsdom": "^26.0.0",
55 | "typescript": "^5.7.2",
56 | "vite": "^6.1.0",
57 | "vitest": "^3.0.5",
58 | "web-vitals": "^4.2.4"
59 | }
60 | }
--------------------------------------------------------------------------------
/src/hooks/useSelectionState.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from "react";
2 |
3 | export interface UseSelectionStateReturn {
4 | selectedIdx: number | null;
5 | selectedType: "service" | "network" | "volume";
6 | selectedNetworkIdx: number | null;
7 | selectedVolumeIdx: number | null;
8 | setSelectedIdx: (idx: number | null) => void;
9 | setSelectedType: (type: "service" | "network" | "volume") => void;
10 | setSelectedNetworkIdx: (idx: number | null) => void;
11 | setSelectedVolumeIdx: (idx: number | null) => void;
12 | selectService: (idx: number | null) => void;
13 | selectNetwork: (idx: number | null) => void;
14 | selectVolume: (idx: number | null) => void;
15 | }
16 |
17 | export function useSelectionState(): UseSelectionStateReturn {
18 | const [selectedIdx, setSelectedIdx] = useState(0);
19 | const [selectedType, setSelectedType] = useState<
20 | "service" | "network" | "volume"
21 | >("service");
22 | const [selectedNetworkIdx, setSelectedNetworkIdx] = useState(
23 | null
24 | );
25 | const [selectedVolumeIdx, setSelectedVolumeIdx] = useState(
26 | null
27 | );
28 |
29 | const selectService = useCallback((idx: number | null) => {
30 | setSelectedIdx(idx);
31 | setSelectedType("service");
32 | setSelectedNetworkIdx(null);
33 | setSelectedVolumeIdx(null);
34 | }, []);
35 |
36 | const selectNetwork = useCallback((idx: number | null) => {
37 | setSelectedNetworkIdx(idx);
38 | setSelectedType("network");
39 | setSelectedIdx(null);
40 | setSelectedVolumeIdx(null);
41 | }, []);
42 |
43 | const selectVolume = useCallback((idx: number | null) => {
44 | setSelectedVolumeIdx(idx);
45 | setSelectedType("volume");
46 | setSelectedIdx(null);
47 | setSelectedNetworkIdx(null);
48 | }, []);
49 |
50 | return {
51 | selectedIdx,
52 | selectedType,
53 | selectedNetworkIdx,
54 | selectedVolumeIdx,
55 | setSelectedIdx,
56 | setSelectedType,
57 | setSelectedNetworkIdx,
58 | setSelectedVolumeIdx,
59 | selectService,
60 | selectNetwork,
61 | selectVolume,
62 | };
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5 |
6 | import { cn } from "../../lib/utils";
7 |
8 | function TooltipProvider({
9 | delayDuration = 0,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
18 | );
19 | }
20 |
21 | function Tooltip({
22 | delayDuration,
23 | ...props
24 | }: React.ComponentProps & {
25 | delayDuration?: number;
26 | }) {
27 | return (
28 |
29 |
30 |
31 | );
32 | }
33 |
34 | function TooltipTrigger({
35 | ...props
36 | }: React.ComponentProps) {
37 | return ;
38 | }
39 |
40 | function TooltipContent({
41 | className,
42 | sideOffset = 0,
43 | children,
44 | ...props
45 | }: React.ComponentProps) {
46 | return (
47 |
48 |
57 | {children}
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
65 |
--------------------------------------------------------------------------------
/src/components/MetaTags.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useRouterState } from "@tanstack/react-router";
3 | import { updateMetaTags } from "../lib/meta-tags";
4 | import type { MetaTagsConfig } from "../lib/meta-tags";
5 |
6 | interface MetaTagsProps {
7 | title?: string;
8 | description?: string;
9 | image?: string;
10 | type?: string;
11 | }
12 |
13 | export function MetaTags({
14 | title,
15 | description,
16 | image,
17 | type,
18 | }: MetaTagsProps) {
19 | const router = useRouterState();
20 | const pathname = router.location.pathname;
21 |
22 | useEffect(() => {
23 | // Get route-specific meta tags or use provided props
24 | const routeMeta = routeMetaTags[pathname];
25 | const config: MetaTagsConfig = {
26 | title: title || routeMeta?.title,
27 | description: description || routeMeta?.description,
28 | image: image || routeMeta?.image,
29 | type: type || routeMeta?.type,
30 | url: typeof window !== "undefined" ? window.location.href : undefined,
31 | };
32 |
33 | updateMetaTags(config);
34 | }, [pathname, title, description, image, type]);
35 |
36 | return null;
37 | }
38 |
39 | // Route-specific meta tag configurations
40 | export const routeMetaTags: Record = {
41 | "/": {
42 | title: "Dock-Dploy - Build Docker Compose Files Without the Hassle",
43 | description:
44 | "A powerful web-based tool for building and managing Docker Compose files, configurations, and schedulers. All in one place, completely free.",
45 | image: "/og-image.png",
46 | },
47 | "/docker/compose-builder": {
48 | title: "Docker Compose Builder - Dock-Dploy",
49 | description:
50 | "Build and manage Docker Compose files with an intuitive interface. Validate, reformat, and convert to various formats.",
51 | image: "/og-image.png",
52 | },
53 | "/config-builder": {
54 | title: "Config Builder - Dock-Dploy",
55 | description:
56 | "Create configuration files for popular self-hosted tools like Homepage.dev and more.",
57 | image: "/og-image.png",
58 | },
59 | "/scheduler-builder": {
60 | title: "Scheduler Builder - Dock-Dploy",
61 | description:
62 | "Generate schedulers for Cron, GitHub Actions, Systemd timers, and more with a simple form.",
63 | image: "/og-image.png",
64 | },
65 | };
66 |
67 |
--------------------------------------------------------------------------------
/src/hooks/useYamlValidation.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useCallback } from "react";
2 | import type { ServiceConfig, NetworkConfig, VolumeConfig } from "../types/compose";
3 | import type { VPNConfig } from "../types/vpn-configs";
4 | import { validateServices } from "../utils/validation";
5 | import { generateYaml } from "../utils/yaml-generator";
6 | import { defaultVPNConfig } from "../utils/default-configs";
7 |
8 | export interface UseYamlValidationOptions {
9 | services: ServiceConfig[];
10 | networks: NetworkConfig[];
11 | volumes: VolumeConfig[];
12 | vpnConfig: VPNConfig;
13 | }
14 |
15 | export function useYamlValidation({
16 | services,
17 | networks,
18 | volumes,
19 | vpnConfig,
20 | }: UseYamlValidationOptions) {
21 | const [yaml, setYaml] = useState("");
22 | const [validationError, setValidationError] = useState(null);
23 | const [validationSuccess, setValidationSuccess] = useState(false);
24 |
25 | // Auto-generate YAML when services, networks, volumes, or vpnConfig change
26 | useEffect(() => {
27 | setYaml(
28 | generateYaml(services, networks, volumes, vpnConfig || defaultVPNConfig())
29 | );
30 | }, [services, networks, volumes, vpnConfig]);
31 |
32 | const validateAndReformat = useCallback(() => {
33 | try {
34 | setValidationError(null);
35 | setValidationSuccess(false);
36 |
37 | // Validate services
38 | const errors = validateServices(services);
39 |
40 | if (errors.length > 0) {
41 | setValidationError(errors.join("; "));
42 | return;
43 | }
44 |
45 | // Regenerate YAML using the imported generateYaml function
46 | // This preserves VPN configs, JSON content, and proper formatting
47 | const reformatted = generateYaml(
48 | services,
49 | networks,
50 | volumes,
51 | vpnConfig || defaultVPNConfig()
52 | );
53 | setYaml(reformatted);
54 | setValidationSuccess(true);
55 | setTimeout(() => setValidationSuccess(false), 3000);
56 | } catch (error: any) {
57 | setValidationError(error.message || "Invalid YAML format");
58 | setValidationSuccess(false);
59 | }
60 | }, [services, networks, volumes, vpnConfig]);
61 |
62 | return {
63 | yaml,
64 | setYaml,
65 | validationError,
66 | validationSuccess,
67 | validateAndReformat,
68 | };
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "../../lib/utils"
4 |
5 | function Card({ className, ...props }: React.ComponentProps<"div">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19 | return (
20 |
28 | )
29 | }
30 |
31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32 | return (
33 |
38 | )
39 | }
40 |
41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42 | return (
43 |
48 | )
49 | }
50 |
51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52 | return (
53 |
61 | )
62 | }
63 |
64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65 | return (
66 |
71 | )
72 | }
73 |
74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75 | return (
76 |
81 | )
82 | }
83 |
84 | export {
85 | Card,
86 | CardHeader,
87 | CardFooter,
88 | CardTitle,
89 | CardAction,
90 | CardDescription,
91 | CardContent,
92 | }
93 |
--------------------------------------------------------------------------------
/src/lib/meta-tags.ts:
--------------------------------------------------------------------------------
1 | export interface MetaTagsConfig {
2 | title?: string;
3 | description?: string;
4 | image?: string;
5 | url?: string;
6 | type?: string;
7 | }
8 |
9 | const DEFAULT_META = {
10 | title: "Dock-Dploy - Build Docker Compose Files Without the Hassle",
11 | description:
12 | "A powerful web-based tool for building and managing Docker Compose files, configurations, and schedulers. All in one place, completely free.",
13 | image: "/og-image.png",
14 | type: "website",
15 | siteName: "Dock-Dploy",
16 | };
17 |
18 | export function updateMetaTags(config: MetaTagsConfig) {
19 | const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
20 | const currentUrl = typeof window !== "undefined" ? window.location.href : "";
21 |
22 | const title = config.title || DEFAULT_META.title;
23 | const description = config.description || DEFAULT_META.description;
24 | const image = config.image
25 | ? config.image.startsWith("http")
26 | ? config.image
27 | : `${baseUrl}${config.image}`
28 | : `${baseUrl}${DEFAULT_META.image}`;
29 | const url = config.url || currentUrl;
30 | const type = config.type || DEFAULT_META.type;
31 |
32 | // Update document title
33 | document.title = title;
34 |
35 | // Helper function to set or update meta tag
36 | const setMetaTag = (
37 | attribute: string,
38 | content: string,
39 | isProperty = false
40 | ) => {
41 | const selector = isProperty
42 | ? `meta[property="${attribute}"]`
43 | : `meta[name="${attribute}"]`;
44 | let element = document.querySelector(selector) as HTMLMetaElement;
45 |
46 | if (!element) {
47 | element = document.createElement("meta");
48 | if (isProperty) {
49 | element.setAttribute("property", attribute);
50 | } else {
51 | element.setAttribute("name", attribute);
52 | }
53 | document.head.appendChild(element);
54 | }
55 |
56 | element.setAttribute("content", content);
57 | };
58 |
59 | // Open Graph tags
60 | setMetaTag("og:title", title, true);
61 | setMetaTag("og:description", description, true);
62 | setMetaTag("og:image", image, true);
63 | setMetaTag("og:url", url, true);
64 | setMetaTag("og:type", type, true);
65 | setMetaTag("og:site_name", DEFAULT_META.siteName, true);
66 |
67 | // Twitter Card tags
68 | setMetaTag("twitter:card", "summary_large_image", false);
69 | setMetaTag("twitter:title", title, false);
70 | setMetaTag("twitter:description", description, false);
71 | setMetaTag("twitter:image", image, false);
72 |
73 | // Standard meta tags
74 | setMetaTag("description", description, false);
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import {Slot} from "@radix-ui/react-slot"
3 | import {cva, type VariantProps} from "class-variance-authority"
4 |
5 | import {cn} from "../../lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16 | outline:
17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 | ghost:
21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22 | link: "text-primary underline-offset-4 hover:underline",
23 | },
24 | size: {
25 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28 | icon: "size-9",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | }
36 | )
37 |
38 | function Button({
39 | className,
40 | variant,
41 | size,
42 | asChild = false,
43 | ...props
44 | }: React.ComponentProps<"button"> &
45 | VariantProps & {
46 | asChild?: boolean
47 | }) {
48 | const Comp = asChild ? Slot : "button"
49 |
50 | return (
51 |
56 | )
57 | }
58 |
59 | export {Button, buttonVariants}
60 |
--------------------------------------------------------------------------------
/src/hooks/useConversionDialog.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from "react";
2 | import type { ServiceConfig } from "../types/compose";
3 | import type { VPNConfig } from "../types/vpn-configs";
4 | import {
5 | convertToDockerRun,
6 | convertToSystemd,
7 | generateKomodoToml,
8 | generateEnvFile,
9 | } from "../utils/converters";
10 | import { redactSensitiveData } from "../utils/validation";
11 |
12 | export type ConversionType = "docker-run" | "systemd" | "env" | "redact" | "komodo";
13 |
14 | export interface UseConversionDialogOptions {
15 | services: ServiceConfig[];
16 | selectedIdx: number | null;
17 | yaml: string;
18 | vpnConfig: VPNConfig;
19 | }
20 |
21 | export function useConversionDialog({
22 | services,
23 | selectedIdx,
24 | yaml,
25 | vpnConfig,
26 | }: UseConversionDialogOptions) {
27 | const [conversionDialogOpen, setConversionDialogOpen] = useState(false);
28 | const [conversionType, setConversionType] = useState("");
29 | const [conversionOutput, setConversionOutput] = useState("");
30 |
31 | const handleConversion = useCallback(
32 | (type: ConversionType) => {
33 | setConversionType(type);
34 | let output = "";
35 |
36 | try {
37 | switch (type) {
38 | case "docker-run":
39 | if (selectedIdx !== null && services[selectedIdx]) {
40 | output = convertToDockerRun(services[selectedIdx]);
41 | } else {
42 | output = services.map((s) => convertToDockerRun(s)).join("\n\n");
43 | }
44 | break;
45 | case "systemd":
46 | if (selectedIdx !== null && services[selectedIdx]) {
47 | output = convertToSystemd(services[selectedIdx]);
48 | } else {
49 | output = services.map((s) => convertToSystemd(s)).join("\n\n");
50 | }
51 | break;
52 | case "env":
53 | output = generateEnvFile(services, vpnConfig);
54 | break;
55 | case "redact":
56 | output = redactSensitiveData(yaml);
57 | break;
58 | case "komodo":
59 | output = generateKomodoToml(yaml);
60 | break;
61 | default:
62 | output = "Unknown conversion type";
63 | }
64 | setConversionOutput(output);
65 | setConversionDialogOpen(true);
66 | } catch (error: any) {
67 | setConversionOutput(`Error: ${error.message}`);
68 | setConversionDialogOpen(true);
69 | }
70 | },
71 | [services, selectedIdx, yaml, vpnConfig]
72 | );
73 |
74 | const closeDialog = useCallback(() => {
75 | setConversionDialogOpen(false);
76 | }, []);
77 |
78 | return {
79 | conversionDialogOpen,
80 | setConversionDialogOpen,
81 | conversionType,
82 | conversionOutput,
83 | handleConversion,
84 | closeDialog,
85 | };
86 | }
87 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cn } from "../../lib/utils"
3 |
4 | const TabsContext = React.createContext<{
5 | value: string
6 | onValueChange: (value: string) => void
7 | }>({
8 | value: "",
9 | onValueChange: () => {},
10 | })
11 |
12 | export interface TabsProps extends React.HTMLAttributes {
13 | value: string
14 | onValueChange: (value: string) => void
15 | }
16 |
17 | export function Tabs({
18 | value,
19 | onValueChange,
20 | className,
21 | children,
22 | ...props
23 | }: TabsProps) {
24 | return (
25 |
26 |
27 | {children}
28 |
29 |
30 | )
31 | }
32 |
33 | export interface TabsListProps extends React.HTMLAttributes {}
34 |
35 | export function TabsList({ className, ...props }: TabsListProps) {
36 | return (
37 |
44 | )
45 | }
46 |
47 | export interface TabsTriggerProps extends React.ButtonHTMLAttributes {
48 | value: string
49 | }
50 |
51 | export function TabsTrigger({ value, className, ...props }: TabsTriggerProps) {
52 | const { value: selectedValue, onValueChange } = React.useContext(TabsContext)
53 | const isSelected = selectedValue === value
54 |
55 | return (
56 |