mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-06-03 20:12:58 +00:00
small fix, don't worry about it
This commit is contained in:
@@ -0,0 +1,631 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Railway Dashboard</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
railway: {
|
||||
bg: '#0d0d12',
|
||||
sidebar: '#0a0a0f',
|
||||
card: '#15151d',
|
||||
cardHover: '#1a1a24',
|
||||
border: '#232330',
|
||||
accent: '#9d4edd',
|
||||
accentHover: '#7b2cbf',
|
||||
success: '#22c55e',
|
||||
muted: '#6b7280',
|
||||
text: '#f3f4f6',
|
||||
textMuted: '#9ca3af'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.connection-line {
|
||||
position: absolute;
|
||||
border: 2px dashed #3d3d55;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.connection-line.horizontal {
|
||||
height: 2px;
|
||||
border-top: 2px dashed #3d3d55;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.service-card {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.group-container {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
animation: dropdownIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
@keyframes dropdownIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-icon {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.sidebar-icon:hover {
|
||||
background-color: rgba(157, 78, 221, 0.1);
|
||||
color: #9d4edd;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.add-btn:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 15px rgba(157, 78, 221, 0.3);
|
||||
}
|
||||
|
||||
/* Modal styles */
|
||||
.modal-backdrop {
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #0a0a0f;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #3d3d55;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #4d4d66;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-railway-bg text-railway-text h-screen overflow-hidden">
|
||||
<div class="flex h-full">
|
||||
<!-- Sidebar -->
|
||||
<aside class="w-16 bg-railway-sidebar border-r border-railway-border flex flex-col items-center py-4 flex-shrink-0">
|
||||
<!-- Logo -->
|
||||
<div class="mb-6">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-railway-accent to-railway-accentHover flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nav Icons -->
|
||||
<nav class="flex flex-col gap-2 flex-1">
|
||||
<button class="sidebar-icon w-10 h-10 rounded-lg flex items-center justify-center text-railway-accent bg-railway-card">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="sidebar-icon w-10 h-10 rounded-lg flex items-center justify-center text-railway-textMuted">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="sidebar-icon w-10 h-10 rounded-lg flex items-center justify-center text-railway-textMuted">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="sidebar-icon w-10 h-10 rounded-lg flex items-center justify-center text-railway-textMuted">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Bottom Icons -->
|
||||
<div class="flex flex-col gap-2 mt-auto">
|
||||
<button class="sidebar-icon w-10 h-10 rounded-lg flex items-center justify-center text-railway-textMuted">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="sidebar-icon w-10 h-10 rounded-lg flex items-center justify-center text-railway-textMuted">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 flex flex-col min-w-0">
|
||||
<!-- Header -->
|
||||
<header class="h-14 border-b border-railway-border flex items-center justify-between px-6 bg-railway-bg/50 backdrop-blur-sm">
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="flex items-center gap-2 text-railway-text hover:text-white transition-colors">
|
||||
<span class="font-semibold">dazzling-curiosity</span>
|
||||
<svg class="w-4 h-4 text-railway-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="text-railway-muted">/</span>
|
||||
<button class="flex items-center gap-2 text-railway-textMuted hover:text-white transition-colors text-sm">
|
||||
<span>production</span>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="add-btn flex items-center gap-2 bg-railway-card hover:bg-railway-cardHover border border-railway-border px-3 py-1.5 rounded-lg text-sm font-medium">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
Add
|
||||
</button>
|
||||
<div class="w-px h-6 bg-railway-border mx-1"></div>
|
||||
<button class="w-8 h-8 rounded-lg flex items-center justify-center text-railway-textMuted hover:text-white hover:bg-railway-card transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="w-8 h-8 rounded-lg flex items-center justify-center text-railway-textMuted hover:text-white hover:bg-railway-card transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Dashboard Content -->
|
||||
<div class="flex-1 overflow-auto p-6">
|
||||
<div id="groups-container" class="flex flex-col gap-6 max-w-7xl mx-auto">
|
||||
<!-- Groups will be rendered here -->
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Add Service Modal -->
|
||||
<div id="add-service-modal" class="fixed inset-0 z-50 hidden">
|
||||
<div class="modal-backdrop absolute inset-0 bg-black/60" onclick="closeModal()"></div>
|
||||
<div class="absolute inset-0 flex items-center justify-center p-4">
|
||||
<div class="bg-railway-card border border-railway-border rounded-xl w-full max-w-md p-6 shadow-2xl">
|
||||
<h3 class="text-lg font-semibold mb-4">Add New Service</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm text-railway-textMuted mb-1">Service Name</label>
|
||||
<input type="text" id="service-name" class="w-full bg-railway-bg border border-railway-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-railway-accent" placeholder="e.g., MyApp">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-railway-textMuted mb-1">Domain</label>
|
||||
<input type="text" id="service-domain" class="w-full bg-railway-bg border border-railway-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-railway-accent" placeholder="e.g., myapp.example.com">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-railway-textMuted mb-1">Group</label>
|
||||
<select id="service-group" class="w-full bg-railway-bg border border-railway-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-railway-accent">
|
||||
<option value="homelab">Homelab</option>
|
||||
<option value="competition">Competition</option>
|
||||
<option value="apis">API's</option>
|
||||
<option value="new">+ New Group</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="new-group-input" class="hidden">
|
||||
<label class="block text-sm text-railway-textMuted mb-1">New Group Name</label>
|
||||
<input type="text" id="new-group-name" class="w-full bg-railway-bg border border-railway-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-railway-accent" placeholder="Group name">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-railway-textMuted mb-1">Icon</label>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button onclick="selectIcon('server')" class="icon-select w-10 h-10 rounded-lg bg-railway-bg border border-railway-border flex items-center justify-center hover:border-railway-accent" data-icon="server">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 01-2 2v4a2 2 0 012 2h14a2 2 0 012-2v-4a2 2 0 01-2-2m-2-4h.01M17 16h.01"></path></svg>
|
||||
</button>
|
||||
<button onclick="selectIcon('database')" class="icon-select w-10 h-10 rounded-lg bg-railway-bg border border-railway-border flex items-center justify-center hover:border-railway-accent" data-icon="database">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg>
|
||||
</button>
|
||||
<button onclick="selectIcon('cloud')" class="icon-select w-10 h-10 rounded-lg bg-railway-bg border border-railway-border flex items-center justify-center hover:border-railway-accent" data-icon="cloud">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"></path></svg>
|
||||
</button>
|
||||
<button onclick="selectIcon('code')" class="icon-select w-10 h-10 rounded-lg bg-railway-bg border border-railway-border flex items-center justify-center hover:border-railway-accent" data-icon="code">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>
|
||||
</button>
|
||||
<button onclick="selectIcon('mail')" class="icon-select w-10 h-10 rounded-lg bg-railway-bg border border-railway-border flex items-center justify-center hover:border-railway-accent" data-icon="mail">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>
|
||||
</button>
|
||||
<button onclick="selectIcon('globe')" class="icon-select w-10 h-10 rounded-lg bg-railway-bg border border-railway-border flex items-center justify-center hover:border-railway-accent" data-icon="globe">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 mt-6">
|
||||
<button onclick="closeModal()" class="px-4 py-2 text-sm text-railway-textMuted hover:text-white transition-colors">Cancel</button>
|
||||
<button onclick="addService()" class="px-4 py-2 bg-railway-accent hover:bg-railway-accentHover text-white rounded-lg text-sm font-medium transition-colors">Add Service</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initial data
|
||||
let groups = [
|
||||
{
|
||||
id: 'homelab',
|
||||
name: 'Homelab',
|
||||
icon: 'home',
|
||||
services: [
|
||||
{
|
||||
id: 'koffan',
|
||||
name: 'Koffan',
|
||||
domain: 'shopping.tdvorak.dev',
|
||||
status: 'online',
|
||||
volume: 'koffan-volume',
|
||||
icon: 'server'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'competition',
|
||||
name: 'Competition',
|
||||
icon: 'trophy',
|
||||
services: [
|
||||
{
|
||||
id: 'insightful-optimism',
|
||||
name: 'insightful-optimism',
|
||||
domain: 'co-back.tdvorak.dev',
|
||||
status: 'online',
|
||||
connections: ['postgres'],
|
||||
icon: 'github'
|
||||
},
|
||||
{
|
||||
id: 'postgres',
|
||||
name: 'Postgres',
|
||||
domain: '',
|
||||
status: 'online',
|
||||
volume: 'postgres-volume-89XR',
|
||||
icon: 'database'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'apis',
|
||||
name: "API's",
|
||||
icon: 'code',
|
||||
services: [
|
||||
{
|
||||
id: 'sendmail',
|
||||
name: 'SendMail',
|
||||
domain: 'sendmail.tdvorak.dev',
|
||||
status: 'online',
|
||||
icon: 'mail'
|
||||
},
|
||||
{
|
||||
id: 'youtubescraper',
|
||||
name: 'YoutubeScraper',
|
||||
domain: 'youtube.tdvorak.dev',
|
||||
status: 'online',
|
||||
icon: 'youtube'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
let selectedIcon = 'server';
|
||||
let dropdownOpen = null;
|
||||
|
||||
// Icon SVGs
|
||||
const icons = {
|
||||
server: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 01-2 2v4a2 2 0 012 2h14a2 2 0 012-2v-4a2 2 0 01-2-2m-2-4h.01M17 16h.01"></path></svg>',
|
||||
database: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg>',
|
||||
cloud: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"></path></svg>',
|
||||
code: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>',
|
||||
mail: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>',
|
||||
globe: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"></path></svg>',
|
||||
home: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>',
|
||||
trophy: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>',
|
||||
github: '<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>',
|
||||
youtube: '<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>'
|
||||
};
|
||||
|
||||
function renderIcon(name) {
|
||||
return icons[name] || icons.server;
|
||||
}
|
||||
|
||||
function getGroupIcon(icon) {
|
||||
const groupIcons = {
|
||||
home: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>',
|
||||
trophy: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 21h8m-4-4v4m-5-16l2.5 9h7L19 5H4zM4 5v6a2 2 0 002 2h2V5H4zm14 0v8h2a2 2 0 002-2V5h-4z"></path></svg>',
|
||||
code: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>'
|
||||
};
|
||||
return groupIcons[icon] || groupIcons.home;
|
||||
}
|
||||
|
||||
function toggleDropdown(groupId, event) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (dropdownOpen === groupId) {
|
||||
closeDropdown();
|
||||
return;
|
||||
}
|
||||
|
||||
closeDropdown();
|
||||
dropdownOpen = groupId;
|
||||
|
||||
const dropdown = document.getElementById(`dropdown-${groupId}`);
|
||||
if (dropdown) {
|
||||
dropdown.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function closeDropdown() {
|
||||
if (dropdownOpen) {
|
||||
const dropdown = document.getElementById(`dropdown-${dropdownOpen}`);
|
||||
if (dropdown) {
|
||||
dropdown.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
dropdownOpen = null;
|
||||
}
|
||||
|
||||
function deleteGroup(groupId) {
|
||||
groups = groups.filter(g => g.id !== groupId);
|
||||
renderGroups();
|
||||
closeDropdown();
|
||||
}
|
||||
|
||||
function deleteService(groupId, serviceId) {
|
||||
const group = groups.find(g => g.id === groupId);
|
||||
if (group) {
|
||||
group.services = group.services.filter(s => s.id !== serviceId);
|
||||
renderGroups();
|
||||
}
|
||||
}
|
||||
|
||||
function renderConnection() {
|
||||
// Connection line between insightful-optimism and postgres
|
||||
return `
|
||||
<div class="hidden md:flex items-center justify-center w-16 relative">
|
||||
<div class="absolute h-0.5 border-t-2 border-dashed border-railway-border w-full"></div>
|
||||
<div class="absolute -left-1 w-2 h-2 rounded-full bg-railway-border"></div>
|
||||
<div class="absolute -right-1 w-2 h-2 rounded-full bg-railway-border"></div>
|
||||
<svg class="w-4 h-4 text-railway-muted absolute right-0 transform translate-x-full" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderServiceCard(service, groupId) {
|
||||
const hasVolume = service.volume ? `
|
||||
<div class="flex items-center gap-2 text-xs text-railway-textMuted mt-3 pt-3 border-t border-railway-border/50">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
|
||||
</svg>
|
||||
${service.volume}
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
return `
|
||||
<div class="service-card bg-railway-card border border-railway-border rounded-lg p-4 min-w-[220px] max-w-[280px] relative group">
|
||||
<button onclick="deleteService('${groupId}', '${service.id}')" class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity text-railway-textMuted hover:text-red-400 p-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="flex items-start gap-3 mb-2">
|
||||
<div class="w-10 h-10 rounded-lg bg-railway-bg flex items-center justify-center text-railway-text shrink-0">
|
||||
${renderIcon(service.icon)}
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h4 class="font-semibold text-sm truncate">${service.name}</h4>
|
||||
<p class="text-xs text-railway-textMuted truncate">${service.domain || 'Internal Service'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full bg-railway-success status-dot"></span>
|
||||
<span class="text-xs text-railway-success font-medium">Online</span>
|
||||
</div>
|
||||
${hasVolume}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderGroup(group) {
|
||||
const servicesHtml = group.services.map((service, index) => {
|
||||
const card = renderServiceCard(service, group.id);
|
||||
// Add connection line if this is the first service and there are 2 services
|
||||
if (group.id === 'competition' && index === 0 && group.services.length > 1) {
|
||||
return card + renderConnection();
|
||||
}
|
||||
return card;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="group-container">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-railway-accent">
|
||||
${getGroupIcon(group.icon)}
|
||||
</div>
|
||||
<h3 class="font-semibold text-railway-text">${group.name}</h3>
|
||||
</div>
|
||||
<button onclick="toggleDropdown('${group.id}', event)" class="relative p-1 text-railway-textMuted hover:text-white transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"></path>
|
||||
</svg>
|
||||
<div id="dropdown-${group.id}" class="dropdown-menu hidden absolute right-0 top-full mt-1 w-32 bg-railway-card border border-railway-border rounded-lg shadow-xl z-10">
|
||||
<button onclick="deleteGroup('${group.id}')" class="w-full text-left px-3 py-2 text-sm text-red-400 hover:bg-railway-bg rounded-lg">Delete Group</button>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
${servicesHtml}
|
||||
<button onclick="openModal('${group.id}')" class="border-2 border-dashed border-railway-border rounded-lg p-4 min-w-[220px] max-w-[280px] flex items-center justify-center text-railway-textMuted hover:text-railway-accent hover:border-railway-accent/50 transition-all group">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<svg class="w-6 h-6 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
<span class="text-sm font-medium">Add Service</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderGroups() {
|
||||
const container = document.getElementById('groups-container');
|
||||
container.innerHTML = groups.map(renderGroup).join('');
|
||||
}
|
||||
|
||||
function openModal(preselectedGroup = null) {
|
||||
const modal = document.getElementById('add-service-modal');
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
if (preselectedGroup) {
|
||||
document.getElementById('service-group').value = preselectedGroup;
|
||||
}
|
||||
|
||||
// Reset form
|
||||
document.getElementById('service-name').value = '';
|
||||
document.getElementById('service-domain').value = '';
|
||||
document.getElementById('new-group-name').value = '';
|
||||
document.getElementById('new-group-input').classList.add('hidden');
|
||||
selectIcon('server');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const modal = document.getElementById('add-service-modal');
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
function selectIcon(icon) {
|
||||
selectedIcon = icon;
|
||||
document.querySelectorAll('.icon-select').forEach(btn => {
|
||||
if (btn.dataset.icon === icon) {
|
||||
btn.classList.add('border-railway-accent', 'text-railway-accent');
|
||||
btn.classList.remove('border-railway-border');
|
||||
} else {
|
||||
btn.classList.remove('border-railway-accent', 'text-railway-accent');
|
||||
btn.classList.add('border-railway-border');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addService() {
|
||||
const name = document.getElementById('service-name').value.trim();
|
||||
const domain = document.getElementById('service-domain').value.trim();
|
||||
const groupSelect = document.getElementById('service-group').value;
|
||||
const newGroupName = document.getElementById('new-group-name').value.trim();
|
||||
|
||||
if (!name) {
|
||||
alert('Please enter a service name');
|
||||
return;
|
||||
}
|
||||
|
||||
let groupId = groupSelect;
|
||||
|
||||
if (groupSelect === 'new') {
|
||||
if (!newGroupName) {
|
||||
alert('Please enter a group name');
|
||||
return;
|
||||
}
|
||||
groupId = 'group-' + Date.now();
|
||||
groups.push({
|
||||
id: groupId,
|
||||
name: newGroupName,
|
||||
icon: 'home',
|
||||
services: []
|
||||
});
|
||||
}
|
||||
|
||||
const service = {
|
||||
id: 'service-' + Date.now(),
|
||||
name: name,
|
||||
domain: domain,
|
||||
status: 'online',
|
||||
icon: selectedIcon
|
||||
};
|
||||
|
||||
const group = groups.find(g => g.id === groupId);
|
||||
if (group) {
|
||||
group.services.push(service);
|
||||
}
|
||||
|
||||
renderGroups();
|
||||
closeModal();
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.addEventListener('click', closeDropdown);
|
||||
|
||||
document.getElementById('service-group').addEventListener('change', function() {
|
||||
const newGroupInput = document.getElementById('new-group-input');
|
||||
if (this.value === 'new') {
|
||||
newGroupInput.classList.remove('hidden');
|
||||
} else {
|
||||
newGroupInput.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector('.add-btn').addEventListener('click', () => openModal());
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeModal();
|
||||
closeDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
// Initial render
|
||||
renderGroups();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user