feat: create internal launchpad for KROW
This commit introduces a new internal launchpad for the KROW project. The launchpad provides a centralized location for accessing applications, resources, and diagrams related to the project. The launchpad includes the following features: - A sidebar navigation menu for accessing different sections of the launchpad. - A home view with links to applications and resources. - A diagram viewer for displaying static SVG and dynamic Mermaid diagrams. - Zoom and pan functionality for diagrams. - Dynamic loading of Mermaid diagrams from a configuration file. This launchpad is designed to improve the discoverability of project resources and provide a more streamlined experience for developers and stakeholders.
This commit is contained in:
479
firebase/internal-launchpad/index.html
Normal file
479
firebase/internal-launchpad/index.html
Normal file
@@ -0,0 +1,479 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KROW Launchpad</title>
|
||||
<link rel="icon" type="image/x-icon" href="favicon.svg">
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
|
||||
<!-- Mermaid -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.6.1/mermaid.min.js"></script>
|
||||
<!-- Custom CSS -->
|
||||
<style>
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.main-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: #ffffff;
|
||||
border-right: 1px solid #dee2e6;
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
transition: color 0.2s, background-color 0.2s;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active,
|
||||
.sidebar .nav-link:hover {
|
||||
color: #0d6efd;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.sidebar-heading {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
padding: 0 1rem;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-subheading {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: #868e96;
|
||||
padding: 0 1rem 0 2rem;
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.nav-link.sub-item {
|
||||
padding-left: 2.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.nav-link.sub-sub-item {
|
||||
padding-left: 3.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#diagram-viewer {
|
||||
display: none;
|
||||
height: calc(100vh - 4rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#diagram-container {
|
||||
cursor: grab;
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
background-color: #ffffff;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #dee2e6;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
#diagram-container:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
#diagram-container:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#diagram-container svg {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="main-layout">
|
||||
<!-- Sidebar -->
|
||||
<nav class="sidebar">
|
||||
<div class="d-flex justify-content-center align-items-center mb-4">
|
||||
<img src="logo.svg" alt="Krow Logo" style="height: 60px;" onerror="this.style.display='none'">
|
||||
</div>
|
||||
<div class="nav flex-column nav-pills" id="sidebar-nav">
|
||||
<a class="nav-link active" href="#" id="nav-home" onclick="showView('home', this)">
|
||||
<i class="bi bi-house-door"></i>Home
|
||||
</a>
|
||||
|
||||
<div class="sidebar-heading">Static Diagrams</div>
|
||||
|
||||
<a class="nav-link" href="#" onclick="showView('diagram', this, './assets/diagrams/high-level-overview.svg', 'Apps High-Level Overview', 'svg')">
|
||||
<i class="bi bi-diagram-3"></i>High-Level Overview
|
||||
</a>
|
||||
<a class="nav-link" href="#" onclick="showView('diagram', this, './assets/diagrams/shift-lifecycle-workflow.svg', 'Core Workflow - Shift Lifecycle', 'svg')">
|
||||
<i class="bi bi-arrow-repeat"></i>Shift Lifecycle
|
||||
</a>
|
||||
<a class="nav-link" href="#" onclick="showView('diagram', this, './assets/diagrams/invoice-workflow.svg', 'Invoice Workflow - Complete', 'svg')">
|
||||
<i class="bi bi-receipt"></i>Invoice Workflow
|
||||
</a>
|
||||
<a class="nav-link" href="#" onclick="showView('diagram', this, './assets/diagrams/complete-workflow.svg', 'Complete Workflow', 'svg')">
|
||||
<i class="bi bi-infinity"></i>Complete Workflow
|
||||
</a>
|
||||
|
||||
<!-- Dynamic Mermaid diagrams will be inserted here -->
|
||||
<div id="dynamic-diagrams-section"></div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="content-area">
|
||||
<!-- View 1: Home Content -->
|
||||
<div id="home-view">
|
||||
<h2 class="mb-4">Welcome to the Project Launchpad</h2>
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header"><h3>Applications</h3></div>
|
||||
<div class="card-body">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<a target="_blank" href="#">Control Tower (Dev)</a>
|
||||
<span class="badge bg-primary rounded-pill">Dev</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<a target="_blank" href="#">Control Tower (Staging)</a>
|
||||
<span class="badge bg-warning text-dark rounded-pill">Staging</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header"><h3>Legacy Mobile Apps</h3></div>
|
||||
<div class="card-body">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<a target="_blank" href="https://play.google.com/store/apps/dev?id=9163719228191263405&hl=en">Google Play Store</a>
|
||||
<span class="badge bg-success rounded-pill">Live</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<a target="_blank" href="https://apps.apple.com/us/developer/thinkloops-llc/id1719034287">Apple App Store</a>
|
||||
<span class="badge bg-success rounded-pill">Live</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header"><h3>Access & Resources</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="list-group list-group-flush">
|
||||
<a href="https://github.com/Oloodi/krow-workforce" target="_blank" class="list-group-item list-group-item-action">
|
||||
<div class="fw-bold">GitHub Repository</div>
|
||||
</a>
|
||||
<a href="https://chat.google.com/" target="_blank" class="list-group-item list-group-item-action">
|
||||
<div class="fw-bold">Team Communication</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- View 2: Diagram Viewer -->
|
||||
<div id="diagram-viewer">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h3 id="diagram-title" class="mb-0"></h3>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-outline-secondary" id="zoomInBtn" title="Zoom In"><i class="bi bi-zoom-in"></i></button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="zoomOutBtn" title="Zoom Out"><i class="bi bi-zoom-out"></i></button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="resetBtn" title="Reset View"><i class="bi bi-aspect-ratio"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="diagram-container" tabindex="-1">
|
||||
<!-- SVG will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@panzoom/panzoom@4.5.1/dist/panzoom.min.js"></script>
|
||||
|
||||
<script>
|
||||
let mermaidDiagrams = [];
|
||||
|
||||
const homeView = document.getElementById('home-view');
|
||||
const diagramViewer = document.getElementById('diagram-viewer');
|
||||
const diagramContainer = document.getElementById('diagram-container');
|
||||
const diagramTitle = document.getElementById('diagram-title');
|
||||
|
||||
const zoomInBtn = document.getElementById('zoomInBtn');
|
||||
const zoomOutBtn = document.getElementById('zoomOutBtn');
|
||||
const resetBtn = document.getElementById('resetBtn');
|
||||
|
||||
let panzoomInstance = null;
|
||||
let currentScale = 1;
|
||||
|
||||
// Initialize Mermaid
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: 'default',
|
||||
flowchart: {
|
||||
useMaxWidth: false,
|
||||
htmlLabels: true,
|
||||
curve: 'basis'
|
||||
}
|
||||
});
|
||||
|
||||
// Build hierarchical structure from paths
|
||||
function buildDiagramHierarchy(diagrams) {
|
||||
const hierarchy = {};
|
||||
|
||||
diagrams.forEach(diagram => {
|
||||
const parts = diagram.path.split('/');
|
||||
// Remove 'assets/diagrams/' prefix
|
||||
const relevantParts = parts.slice(2, -1); // Everything except filename
|
||||
const filename = parts[parts.length - 1].replace('.mermaid', '');
|
||||
|
||||
let current = hierarchy;
|
||||
relevantParts.forEach(part => {
|
||||
if (!current[part]) {
|
||||
current[part] = { _items: [], _children: {} };
|
||||
}
|
||||
current = current[part]._children;
|
||||
});
|
||||
|
||||
// Add the item to the last level
|
||||
const lastLevel = relevantParts.length > 0 ?
|
||||
relevantParts.reduce((acc, part) => acc[part]._children, hierarchy) :
|
||||
hierarchy;
|
||||
|
||||
if (relevantParts.length > 0) {
|
||||
const parentKey = relevantParts[relevantParts.length - 1];
|
||||
if (!hierarchy[relevantParts[0]]) {
|
||||
hierarchy[relevantParts[0]] = { _items: [], _children: {} };
|
||||
}
|
||||
let parent = hierarchy[relevantParts[0]];
|
||||
for (let i = 1; i < relevantParts.length; i++) {
|
||||
parent = parent._children[relevantParts[i]];
|
||||
}
|
||||
parent._items.push(diagram);
|
||||
} else {
|
||||
if (!hierarchy._root) {
|
||||
hierarchy._root = { _items: [], _children: {} };
|
||||
}
|
||||
hierarchy._root._items.push(diagram);
|
||||
}
|
||||
});
|
||||
|
||||
return hierarchy;
|
||||
}
|
||||
|
||||
// Create navigation from hierarchy
|
||||
function createNavigation(hierarchy, parentElement, level = 0) {
|
||||
Object.keys(hierarchy).forEach(key => {
|
||||
if (key === '_items' || key === '_children' || key === '_root') return;
|
||||
|
||||
const section = hierarchy[key];
|
||||
const heading = document.createElement('div');
|
||||
heading.className = level === 0 ? 'sidebar-heading' : 'sidebar-subheading';
|
||||
heading.textContent = key.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
||||
parentElement.appendChild(heading);
|
||||
|
||||
// Add items in this section
|
||||
if (section._items && section._items.length > 0) {
|
||||
section._items.forEach(diagram => {
|
||||
const link = document.createElement('a');
|
||||
link.className = `nav-link ${level > 0 ? 'sub-item' : ''}`;
|
||||
link.href = '#';
|
||||
link.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
showView('diagram', link, diagram.path, diagram.title, 'mermaid');
|
||||
};
|
||||
link.innerHTML = `<i class="bi bi-file-earmark-code"></i>${diagram.title}`;
|
||||
parentElement.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
// Recursively add children
|
||||
if (section._children && Object.keys(section._children).length > 0) {
|
||||
createNavigation(section._children, parentElement, level + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load and render dynamic diagrams navigation
|
||||
async function loadDynamicDiagrams() {
|
||||
const dynamicSection = document.getElementById('dynamic-diagrams-section');
|
||||
|
||||
try {
|
||||
// Fetch the diagrams configuration from JSON file
|
||||
const response = await fetch('./assets/diagrams/diagrams-config.json');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load diagrams config: ${response.status}`);
|
||||
}
|
||||
|
||||
mermaidDiagrams = await response.json();
|
||||
|
||||
if (mermaidDiagrams.length > 0) {
|
||||
const mainHeading = document.createElement('div');
|
||||
mainHeading.className = 'sidebar-heading';
|
||||
mainHeading.textContent = 'Mermaid Diagrams';
|
||||
dynamicSection.appendChild(mainHeading);
|
||||
|
||||
const hierarchy = buildDiagramHierarchy(mermaidDiagrams);
|
||||
createNavigation(hierarchy, dynamicSection);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading diagrams configuration:', error);
|
||||
dynamicSection.innerHTML = `<div class="alert alert-warning m-2 small">No Mermaid diagrams configuration found</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveNav(activeLink) {
|
||||
document.querySelectorAll('.sidebar .nav-link').forEach(link => {
|
||||
link.classList.remove('active');
|
||||
});
|
||||
activeLink.classList.add('active');
|
||||
}
|
||||
|
||||
async function showView(viewName, navLink, filePath, title, type = 'svg') {
|
||||
setActiveNav(navLink);
|
||||
if (panzoomInstance) {
|
||||
panzoomInstance.destroy();
|
||||
panzoomInstance = null;
|
||||
}
|
||||
diagramContainer.innerHTML = '';
|
||||
currentScale = 1;
|
||||
|
||||
if (viewName === 'home') {
|
||||
homeView.style.display = 'block';
|
||||
diagramViewer.style.display = 'none';
|
||||
} else if (viewName === 'diagram') {
|
||||
homeView.style.display = 'none';
|
||||
diagramViewer.style.display = 'flex';
|
||||
diagramTitle.textContent = title;
|
||||
diagramContainer.innerHTML = '<div class="loading-spinner"><div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div></div>';
|
||||
|
||||
try {
|
||||
if (type === 'svg') {
|
||||
// Load static SVG files
|
||||
const response = await fetch(filePath);
|
||||
if (!response.ok) throw new Error(`Network error: ${response.status}`);
|
||||
const svgContent = await response.text();
|
||||
|
||||
const parser = new DOMParser();
|
||||
const svgDoc = parser.parseFromString(svgContent, "image/svg+xml");
|
||||
const svgElement = svgDoc.querySelector('svg');
|
||||
|
||||
if (svgElement) {
|
||||
diagramContainer.innerHTML = '';
|
||||
diagramContainer.appendChild(svgElement);
|
||||
panzoomInstance = Panzoom(svgElement, {
|
||||
canvas: true,
|
||||
maxScale: 10,
|
||||
minScale: 0.3,
|
||||
startScale: 1
|
||||
});
|
||||
diagramContainer.addEventListener('wheel', panzoomInstance.zoomWithWheel);
|
||||
diagramContainer.focus();
|
||||
} else {
|
||||
throw new Error('No SVG element found.');
|
||||
}
|
||||
} else if (type === 'mermaid') {
|
||||
// Load and render Mermaid files
|
||||
const response = await fetch(filePath);
|
||||
if (!response.ok) throw new Error(`Network error: ${response.status}`);
|
||||
const mermaidCode = await response.text();
|
||||
|
||||
const { svg } = await mermaid.render('mermaidDiagram_' + Date.now(), mermaidCode);
|
||||
|
||||
diagramContainer.innerHTML = svg;
|
||||
const svgElement = diagramContainer.querySelector('svg');
|
||||
|
||||
if (svgElement) {
|
||||
svgElement.style.maxWidth = 'none';
|
||||
svgElement.style.height = 'auto';
|
||||
|
||||
panzoomInstance = Panzoom(svgElement, {
|
||||
canvas: true,
|
||||
maxScale: 10,
|
||||
minScale: 0.3,
|
||||
startScale: 1
|
||||
});
|
||||
diagramContainer.addEventListener('wheel', panzoomInstance.zoomWithWheel);
|
||||
diagramContainer.focus();
|
||||
} else {
|
||||
throw new Error('Failed to render Mermaid diagram.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
diagramContainer.innerHTML = `<div class="alert alert-danger m-3">Failed to load diagram: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
zoomInBtn.addEventListener('click', () => {
|
||||
if (panzoomInstance) {
|
||||
panzoomInstance.zoomIn();
|
||||
diagramContainer.focus();
|
||||
}
|
||||
});
|
||||
|
||||
zoomOutBtn.addEventListener('click', () => {
|
||||
if (panzoomInstance) {
|
||||
panzoomInstance.zoomOut();
|
||||
diagramContainer.focus();
|
||||
}
|
||||
});
|
||||
|
||||
resetBtn.addEventListener('click', () => {
|
||||
if (panzoomInstance) {
|
||||
panzoomInstance.reset();
|
||||
diagramContainer.focus();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadDynamicDiagrams();
|
||||
showView('home', document.getElementById('nav-home'));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user