├── .github └── workflows │ └── deploy.yml ├── LICENSE ├── README.md └── frontend ├── .cta.json ├── .env.example ├── .gitignore ├── components.json ├── index.html ├── nginx.conf ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── manifest.json └── robots.txt ├── src ├── components │ ├── CodeEditor.tsx │ ├── SidebarUI.tsx │ ├── ThemeProvider.tsx │ └── ui │ │ ├── alert.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── collapsible.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── textarea.tsx │ │ ├── toggle.tsx │ │ └── tooltip.tsx ├── hooks │ └── use-mobile.ts ├── lib │ └── utils.ts ├── logo.svg ├── main.tsx ├── reportWebVitals.ts ├── routeTree.gen.ts ├── routes │ ├── __root.tsx │ ├── docker │ │ └── compose-builder.tsx │ ├── index.tsx │ ├── privacy.tsx │ └── terms-of-service.tsx └── styles.css ├── tsconfig.json └── vite.config.ts /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy React to VPS 2 | 3 | on: 4 | push: 5 | branches: 6 | - development 7 | paths-ignore: 8 | - '**.md' 9 | - '.gitignore' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build-and-deploy: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout repo 18 | uses: actions/checkout@v3 19 | 20 | - name: Install Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: '18' 24 | 25 | - name: Install frontend dependencies & build 26 | run: | 27 | cd frontend 28 | npm ci 29 | npm run build 30 | 31 | - name: Deploy frontend and backend files via SCP 32 | uses: appleboy/scp-action@master 33 | with: 34 | host: ${{ secrets.VPS_HOST }} 35 | username: ${{ secrets.VPS_USER }} 36 | key: ${{ secrets.VPS_SSH_KEY }} 37 | source: | 38 | frontend/build/* 39 | src/backend/database.cjs 40 | package.json 41 | package-lock.json 42 | target: /var/www/dashix.dev 43 | 44 | - name: Restart backend with systemd 45 | uses: appleboy/ssh-action@master 46 | with: 47 | host: ${{ secrets.VPS_HOST }} 48 | username: ${{ secrets.VPS_USER }} 49 | key: ${{ secrets.VPS_SSH_KEY }} 50 | script: | 51 | sudo systemctl restart dashix-backend 52 | 53 | - name: Install backend dependencies 54 | uses: appleboy/ssh-action@master 55 | with: 56 | host: ${{ secrets.VPS_HOST }} 57 | username: ${{ secrets.VPS_USER }} 58 | key: ${{ secrets.VPS_SSH_KEY }} 59 | script: | 60 | cd /var/www/dashix.dev 61 | npm install 62 | 63 | - name: Restart nginx 64 | uses: appleboy/ssh-action@master 65 | with: 66 | host: ${{ secrets.VPS_HOST }} 67 | username: ${{ secrets.VPS_USER }} 68 | key: ${{ secrets.VPS_SSH_KEY }} 69 | script: | 70 | sudo service nginx restart 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Luke Gustafson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Repo Stats 2 | ![GitHub Repo stars](https://img.shields.io/github/stars/LukeGus/Dashix?style=flat&label=Stars) 3 | ![GitHub forks](https://img.shields.io/github/forks/LukeGus/Dashix?style=flat&label=Forks) 4 | ![GitHub Release](https://img.shields.io/github/v/release/LukeGus/Dashix?style=flat&label=Release) 5 | Discord 6 | #### Top Technologies 7 | [![React Badge](https://img.shields.io/badge/-React-61DBFB?style=flat-square&labelColor=black&logo=react&logoColor=61DBFB)](#) 8 | [![Typescript Badge](https://img.shields.io/badge/-Typescript-3178c6?style=flat-square&labelColor=black&logo=typescript&logoColor=3178c6)](#) 9 | [![Nodejs Badge](https://img.shields.io/badge/-Nodejs-3C873A?style=flat-square&labelColor=black&logo=node.js&logoColor=3C873A)](#) 10 | [![HTML Badge](https://img.shields.io/badge/-HTML-E34F26?style=flat-square&labelColor=black&logo=html5&logoColor=E34F26)](#) 11 | [![Tailwind CSS Badge](https://img.shields.io/badge/-TailwindCSS-38B2AC?style=flat-square&labelColor=black&logo=tailwindcss&logoColor=38B2AC)](#) 12 | [![Docker Badge](https://img.shields.io/badge/-Docker-2496ED?style=flat-square&labelColor=black&logo=docker&logoColor=2496ED)](#) 13 | [![MongoDB Badge](https://img.shields.io/badge/-MongoDB-47A248?style=flat-square&labelColor=black&logo=mongodb&logoColor=47A248)](#) 14 | [![ShadCN Badge](https://img.shields.io/badge/-ShadCN_UI-111827?style=flat-square&labelColor=black&logo=tailwindcss&logoColor=38BDF8)](#) 15 | [![Express Badge](https://img.shields.io/badge/-Express.js-000000?style=flat-square&labelColor=black&logo=express&logoColor=white)](#) 16 | 17 |
18 |

19 | 20 | Dashix Banner 21 |

22 | 23 | If you would like, you can support the project here!\ 24 | [![PayPal](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://paypal.me/LukeGustafson803) 25 | 26 | ### Other Projects 27 | - **[Termix](https://github.com/LukeGus/Termix)** - Clientless web-based SSH terminal emulator that stores and manages your connection details 28 | - **[Tunnelix](https://github.com/LukeGus/Tunnelix)** - Clientless web-based reverse SSH control panel that stores and manages your tunnels through SSH 29 | - **[Confix](https://github.com/LukeGus/Confix)** - Self-hosted config file manager with persistent session history and quick access 30 | 31 | # Overview 32 | [Dashix](https://www.dashix.dev/) is a website offering a suite of tools for self-hosters to manage Docker Compose, configs, scheduled tasks, and more with ease. 33 | 34 | # Features 35 | - Docker-compose builder (drag/drop system to configure and create a docker-compose file) 36 | - With [Containix](https://github.com/LukeGus/Containix) support (view a list of popular docker-compose files and easily modify them) 37 | - Built-in feedback system 38 | 39 | # Planned Features 40 | - Self-hosted version 41 | - Docker Compose Builder (build upon the existing version) 42 | - AI-generated Compose files 43 | - Validate & reformat Compose files 44 | - Convert Compose to: 45 | - docker run 46 | - systemd 47 | - .env usage 48 | - Generate Komodo .toml from portainer stacks 49 | - Redact sensitive data in a compose file/config for sharing 50 | - Config Builder (gethomepage.dev, and more) 51 | - Scheduler Builder (Cron, GitHub Actions, Systemd, etc.) 52 | - Select time, command, file name, etc. 53 | - Quick Web-SSH (web-based SSH access) 54 | 55 | # Installation 56 | Dashix currently only exists as a public website, however a self-hosted version is planned. 57 | 58 | # Support 59 | If you need help with Dashix, you can join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support channel. You can also open an issue or open a pull request on the [GitHub](https://github.com/LukeGus/Dashix/issues) repo or use the built-in feedback system on the Dashix sidebar. 60 | If you would like to support me financially, you can on [PayPal](https://paypal.me/LukeGustafson803). 61 | 62 | # License 63 | Distributed under the MIT license. See LICENSE for more information. 64 | -------------------------------------------------------------------------------- /frontend/.cta.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "Dashix", 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": [ 11 | "biome", 12 | "shadcn" 13 | ] 14 | } -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | VITE_ENV=prod 2 | VITE_SELF_HOSTED=true -------------------------------------------------------------------------------- /frontend/.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 | -------------------------------------------------------------------------------- /frontend/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 | } -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Dashix 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/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 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dashix", 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.1.1", 15 | "@radix-ui/react-collapsible": "^1.1.11", 16 | "@radix-ui/react-dialog": "^1.1.14", 17 | "@radix-ui/react-dropdown-menu": "^2.1.15", 18 | "@radix-ui/react-label": "^2.1.7", 19 | "@radix-ui/react-separator": "^1.1.7", 20 | "@radix-ui/react-slot": "^1.2.3", 21 | "@radix-ui/react-toggle": "^1.1.9", 22 | "@radix-ui/react-tooltip": "^1.2.7", 23 | "@tailwindcss/vite": "^4.0.6", 24 | "@tanstack/react-router": "^1.121.2", 25 | "@tanstack/react-router-devtools": "^1.121.2", 26 | "@tanstack/router-plugin": "^1.121.2", 27 | "@uiw/codemirror-extensions-hyper-link": "^4.23.14", 28 | "@uiw/codemirror-theme-monokai-dimmed": "^4.23.14", 29 | "@uiw/react-codemirror": "^4.23.14", 30 | "axios": "^1.10.0", 31 | "class-variance-authority": "^0.7.1", 32 | "clsx": "^2.1.1", 33 | "js-yaml": "^4.1.0", 34 | "lucide-react": "^0.476.0", 35 | "react": "^19.0.0", 36 | "react-dom": "^19.0.0", 37 | "react-hook-form": "^7.60.0", 38 | "tailwind-merge": "^3.0.2", 39 | "tailwindcss": "^4.0.6", 40 | "tailwindcss-animate": "^1.0.7", 41 | "zod": "^4.0.2" 42 | }, 43 | "devDependencies": { 44 | "@biomejs/biome": "1.9.4", 45 | "@testing-library/dom": "^10.4.0", 46 | "@testing-library/react": "^16.2.0", 47 | "@types/js-yaml": "^4.0.9", 48 | "@types/node": "^24.0.10", 49 | "@types/react": "^19.0.8", 50 | "@types/react-dom": "^19.0.3", 51 | "@vitejs/plugin-react": "^4.3.4", 52 | "jsdom": "^26.0.0", 53 | "typescript": "^5.7.2", 54 | "vite": "^6.1.0", 55 | "vitest": "^3.0.5", 56 | "web-vitals": "^4.2.4" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shantnudon/Dashix/bdf375b1484099cefc22888021c60240cdf6e45b/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "TanStack App", 3 | "name": "Create TanStack App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "256x256 128x128 64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /frontend/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 -------------------------------------------------------------------------------- /frontend/src/components/CodeEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import CodeMirror from "@uiw/react-codemirror"; 3 | import { hyperLink } from '@uiw/codemirror-extensions-hyper-link'; 4 | import {yaml} from "@codemirror/lang-yaml"; 5 | import { Button } from "./ui/button"; 6 | import { monokaiDimmed } from '@uiw/codemirror-theme-monokai-dimmed'; 7 | 8 | interface CodeEditorProps { 9 | content: string; 10 | onContentChange: (value: string) => void; 11 | width?: number; 12 | height?: number; 13 | } 14 | 15 | export const CodeEditor: React.FC = ({ 16 | content, 17 | onContentChange, 18 | width, 19 | height, 20 | }) => { 21 | const [copied, setCopied] = useState(false); 22 | 23 | const handleCopy = async () => { 24 | try { 25 | await navigator.clipboard.writeText(content); 26 | setCopied(true); 27 | setTimeout(() => setCopied(false), 1200); 28 | } catch (e) { 29 | setCopied(false); 30 | } 31 | }; 32 | 33 | return ( 34 |
48 | {/* Copy button in top right */} 49 | 61 | onContentChange(value)} 72 | basicSetup={{ lineNumbers: true }} 73 | style={{ 74 | flex: 1, 75 | minHeight: 0, 76 | minWidth: 0, 77 | fontSize: 16, 78 | }} 79 | /> 80 |
81 | ); 82 | }; -------------------------------------------------------------------------------- /frontend/src/components/SidebarUI.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ChevronDown, 3 | Container, 4 | AlertCircleIcon, 5 | CheckCircle2Icon, 6 | } from "lucide-react"; 7 | 8 | import { useNavigate, useRouter } from "@tanstack/react-router"; 9 | 10 | import { 11 | SidebarContent, 12 | SidebarGroup, 13 | SidebarGroupContent, 14 | SidebarGroupLabel, 15 | SidebarMenu, 16 | SidebarMenuButton, 17 | SidebarMenuItem, 18 | SidebarHeader, 19 | SidebarFooter, 20 | } from "./ui/sidebar"; 21 | 22 | import { 23 | Collapsible, 24 | CollapsibleTrigger, 25 | CollapsibleContent, 26 | } from "./ui/collapsible"; 27 | 28 | import { Separator } from "./ui/separator"; 29 | import { Button } from "./ui/button"; 30 | import { useState, useEffect, useRef } from "react"; 31 | import { CardContent } from "./ui/card"; 32 | import { 33 | Form, 34 | FormControl, 35 | FormField, 36 | FormItem, 37 | FormMessage, 38 | FormLabel, 39 | } from "./ui/form"; 40 | import { useForm } from "react-hook-form"; 41 | import { Textarea } from "./ui/textarea"; 42 | import axios from "axios"; 43 | import { Alert, AlertTitle, AlertDescription } from "./ui/alert"; 44 | import { Input } from "./ui/input"; 45 | 46 | const items = [ 47 | { 48 | title: "Compose Builder", 49 | url: "/docker/compose-builder", 50 | icon: Container, 51 | group: "Docker", 52 | }, 53 | ]; 54 | 55 | const groupedItems = items.reduce>((acc, item) => { 56 | if (!acc[item.group]) acc[item.group] = []; 57 | acc[item.group].push(item); 58 | return acc; 59 | }, {}); 60 | 61 | export function SidebarUI({}: {}) { 62 | const navigate = useNavigate(); 63 | const router = useRouter(); 64 | const location = router.state.location; 65 | const showFooterLinks = !( 66 | import.meta.env.VITE_ENV === "prod" && 67 | import.meta.env.VITE_SELF_HOSTED === "true" 68 | ); 69 | const [showFeedbackCard, setShowFeedbackCard] = useState(false); 70 | const [feedbackSent, setFeedbackSent] = useState(false); 71 | const feedbackForm = useForm({ 72 | defaultValues: { title: "", feedback: "", honey: "" }, 73 | }); 74 | const turnstileWidgetRef = useRef(null); 75 | const [captchaToken, setCaptchaToken] = useState(""); 76 | const timerRef = useRef(null); 77 | const [feedbackError, setFeedbackError] = useState(""); 78 | 79 | useEffect(() => { 80 | if (!document.getElementById("cf-turnstile-script")) { 81 | const script = document.createElement("script"); 82 | script.id = "cf-turnstile-script"; 83 | script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js"; 84 | script.async = true; 85 | script.defer = true; 86 | document.body.appendChild(script); 87 | } 88 | }, []); 89 | 90 | useEffect(() => { 91 | if ( 92 | showFeedbackCard && 93 | (window as any).turnstile && 94 | turnstileWidgetRef.current 95 | ) { 96 | turnstileWidgetRef.current.innerHTML = ""; 97 | (window as any).turnstile.render(turnstileWidgetRef.current, { 98 | sitekey: "0x4AAAAAABkpdrsssAICPqnw", 99 | callback: (token: string) => setCaptchaToken(token), 100 | "error-callback": () => setCaptchaToken(""), 101 | "expired-callback": () => setCaptchaToken(""), 102 | }); 103 | } 104 | if (!showFeedbackCard) setCaptchaToken(""); 105 | }, [showFeedbackCard]); 106 | 107 | useEffect(() => { 108 | if (showFeedbackCard) { 109 | setFeedbackSent(false); 110 | feedbackForm.reset(); 111 | setCaptchaToken(""); 112 | } else { 113 | if (timerRef.current) { 114 | clearTimeout(timerRef.current); 115 | timerRef.current = null; 116 | } 117 | } 118 | }, [showFeedbackCard]); 119 | 120 | const onSubmit = (data: { 121 | title: string; 122 | feedback: string; 123 | honey?: string; 124 | }) => { 125 | if (!captchaToken) { 126 | alert("Please complete the CAPTCHA."); 127 | return; 128 | } 129 | setFeedbackSent(false); 130 | setFeedbackError(""); 131 | let apiUrl; 132 | if ( 133 | window.location.hostname === "localhost" || 134 | window.location.hostname === "127.0.0.1" 135 | ) { 136 | apiUrl = "http://localhost:2000/feedback"; 137 | } else { 138 | apiUrl = `${window.location.origin}/feedback`; 139 | } 140 | axios 141 | .post( 142 | apiUrl, 143 | { 144 | title: data.title, 145 | feedback: data.feedback, 146 | honey: data.honey, 147 | date: new Date().toLocaleString(), 148 | "cf-turnstile-response": captchaToken, 149 | }, 150 | { 151 | headers: { 152 | "Content-Type": "application/json", 153 | }, 154 | } 155 | ) 156 | .then(() => { 157 | setFeedbackSent(true); 158 | timerRef.current = setTimeout(() => { 159 | setShowFeedbackCard(false); 160 | setFeedbackSent(false); 161 | feedbackForm.reset(); 162 | setCaptchaToken(""); 163 | timerRef.current = null; 164 | }, 1500); 165 | }) 166 | .catch((err) => { 167 | if ( 168 | err.response && 169 | (err.response.status === 429 || 170 | (err.response.data && 171 | err.response.data.error && 172 | err.response.data.error.includes( 173 | "Too many feedback submissions" 174 | ))) 175 | ) { 176 | setFeedbackError( 177 | "You have made too many feedback requests. Please try again later." 178 | ); 179 | } else { 180 | setFeedbackError("Failed to send feedback. Please try again."); 181 | } 182 | }); 183 | }; 184 | 185 | useEffect(() => { 186 | (window as any).onTurnstileSuccess = (token: string) => { 187 | setCaptchaToken(token); 188 | }; 189 | }, []); 190 | 191 | return ( 192 | <> 193 | 194 |
195 |
196 | Dashix 197 |
198 | 199 |
200 |
201 | 202 | 203 | {Object.entries(groupedItems).map(([groupName, groupItems], idx) => ( 204 | 209 | 210 | 211 | 212 | {groupName} 213 | 214 | 215 | 216 | 217 | 218 | 219 | {groupItems.map((item) => { 220 | const isActive = location.pathname === item.url; 221 | 222 | return ( 223 | 224 | { 227 | if (!isActive) { 228 | navigate({ to: item.url }); 229 | } 230 | }} 231 | > 232 | 233 | {item.title} 234 | 235 | 236 | ); 237 | })} 238 | 239 | 240 | 241 | 242 | 243 | ))} 244 | 245 | 246 | 247 |
248 | 256 | 265 | 266 | {showFooterLinks && ( 267 | <> 268 | 269 | 276 | 277 | 284 | 285 | 292 | 293 | )} 294 |
295 |
296 | 297 | {showFeedbackCard && ( 298 |
setShowFeedbackCard(false)} 301 | > 302 |
e.stopPropagation()} 305 | > 306 | 313 |
Feedback
314 | 315 | {feedbackError && ( 316 | 321 | 325 |
326 | 327 | Error 328 | 329 | 338 | {feedbackError} 339 | 340 |
341 |
342 | )} 343 | {!feedbackError && feedbackSent && ( 344 | 348 | 352 |
353 | 354 | Thank you for your feedback! 355 | 356 | 365 | Your feedback was submitted successfully. 366 | 367 |
368 |
369 | )} 370 | {!feedbackError && !feedbackSent && ( 371 |
372 | 376 | {/* Honeypot field for bots */} 377 | 384 | {/* Title input */} 385 | ( 389 | 390 | Title 391 | 392 | 397 | 398 | 399 | 400 | )} 401 | /> 402 | {/* Feedback textarea with label */} 403 | 404 | Body 405 | ( 409 | 410 |