Skip to content

Instantly share code, notes, and snippets.

@zilveer
Last active July 1, 2025 18:40
Show Gist options
  • Save zilveer/0c639bcc4e3d8528c1a868d2166b658f to your computer and use it in GitHub Desktop.
Save zilveer/0c639bcc4e3d8528c1a868d2166b658f to your computer and use it in GitHub Desktop.
A58
A58
---
A [Pen](https://codepen.io/zilveer/pen/xbGNKeQ) by [zilveer](https://codepen.io/zilveer) on [CodePen](https://codepen.io).
[License](https://codepen.io/license/pen/xbGNKeQ).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Minimal SPA Router</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #333;
line-height: 1.6;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
margin-bottom: 30px;
text-align: center;
}
h1 {
font-size: 2.5rem;
font-weight: 300;
color: white;
text-shadow: 0 2px 10px rgba(0,0,0,0.3);
margin-bottom: 10px;
}
.subtitle {
color: rgba(255,255,255,0.8);
font-size: 1.1rem;
font-weight: 300;
}
.main-nav {
background: rgba(255,255,255,0.95);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 0;
margin-bottom: 20px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
border: 1px solid rgba(255,255,255,0.2);
}
.nav-list {
list-style: none;
display: flex;
margin: 0;
padding: 0;
overflow-x: auto;
}
.nav-link {
display: block;
padding: 18px 24px;
text-decoration: none;
color: #666;
border-bottom: 3px solid transparent;
transition: all 0.3s ease;
font-weight: 500;
white-space: nowrap;
position: relative;
}
.nav-link:hover {
color: #2c3e50;
background: rgba(52, 152, 219, 0.1);
transform: translateY(-2px);
}
.nav-link.active {
color: #3498db;
border-bottom-color: #3498db;
background: rgba(52, 152, 219, 0.1);
}
.nav-link.active::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, #3498db, #2980b9);
}
.content-area, .tab-content {
background: rgba(255,255,255,0.95);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
border: 1px solid rgba(255,255,255,0.2);
min-height: 200px;
transition: all 0.3s ease;
}
.content-area:hover, .tab-content:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0,0,0,0.15);
}
.sub-nav {
margin: 20px 0;
padding: 0;
list-style: none;
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.sub-nav .nav-link {
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border: 1px solid rgba(255,255,255,0.3);
border-radius: 25px;
padding: 10px 18px;
font-size: 0.9rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
.sub-nav .nav-link:hover {
background: linear-gradient(135deg, #e9ecef, #dee2e6);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.sub-nav .nav-link.active {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
border-color: #2980b9;
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
}
.actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.btn {
padding: 12px 24px;
border: 1px solid rgba(255,255,255,0.3);
background: rgba(255,255,255,0.9);
color: #666;
text-decoration: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.9rem;
font-weight: 500;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
backdrop-filter: blur(10px);
}
.btn:hover {
background: rgba(255,255,255,1);
border-color: #3498db;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.btn-primary {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
border-color: #2980b9;
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
}
.btn-primary:hover {
background: linear-gradient(135deg, #2980b9, #1f5f7a);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(52, 152, 219, 0.4);
}
.btn-success {
background: linear-gradient(135deg, #27ae60, #229954);
color: white;
border-color: #229954;
box-shadow: 0 4px 12px rgba(39, 174, 96, 0.3);
}
.btn-warning {
background: linear-gradient(135deg, #f39c12, #e67e22);
color: white;
border-color: #e67e22;
box-shadow: 0 4px 12px rgba(243, 156, 18, 0.3);
}
.btn-danger {
background: linear-gradient(135deg, #e74c3c, #c0392b);
color: white;
border-color: #c0392b;
box-shadow: 0 4px 12px rgba(231, 76, 60, 0.3);
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.6);
backdrop-filter: blur(5px);
z-index: 1000;
animation: fadeIn 0.3s ease;
}
.modal.show {
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: rgba(255,255,255,0.95);
backdrop-filter: blur(20px);
border-radius: 16px;
padding: 35px;
max-width: 500px;
width: 90%;
position: relative;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
border: 1px solid rgba(255,255,255,0.3);
animation: slideIn 0.3s ease;
}
.modal-close {
position: absolute;
top: 20px;
right: 25px;
background: none;
border: none;
font-size: 1.8rem;
cursor: pointer;
color: #999;
transition: all 0.2s ease;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.modal-close:hover {
color: #e74c3c;
background: rgba(231, 76, 60, 0.1);
transform: rotate(90deg);
}
.offcanvas {
position: fixed;
top: 0;
left: -350px;
width: 350px;
height: 100%;
background: rgba(255,255,255,0.95);
backdrop-filter: blur(20px);
box-shadow: 4px 0 20px rgba(0,0,0,0.15);
transition: left 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
z-index: 1001;
padding: 25px;
border-right: 1px solid rgba(255,255,255,0.3);
overflow-y: auto;
}
.offcanvas.show {
left: 0;
}
.offcanvas-backdrop {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.6);
backdrop-filter: blur(5px);
z-index: 1000;
animation: fadeIn 0.3s ease;
}
.offcanvas-backdrop.show {
display: block;
}
.offcanvas-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 2px solid rgba(52, 152, 219, 0.2);
}
.offcanvas-title {
font-size: 1.4rem;
font-weight: 600;
color: #2c3e50;
margin: 0;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #2c3e50;
font-size: 0.9rem;
}
.form-control {
width: 100%;
padding: 12px 16px;
border: 2px solid rgba(255,255,255,0.3);
border-radius: 8px;
font-size: 1rem;
background: rgba(255,255,255,0.8);
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.form-control:focus {
outline: none;
border-color: #3498db;
background: rgba(255,255,255,0.95);
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
.form-control:hover {
border-color: rgba(52, 152, 219, 0.5);
}
select.form-control {
cursor: pointer;
}
textarea.form-control {
resize: vertical;
min-height: 80px;
}
.status-indicator {
position: fixed;
top: 25px;
right: 25px;
background: rgba(255,255,255,0.95);
backdrop-filter: blur(20px);
padding: 12px 18px;
border-radius: 25px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
font-size: 0.85rem;
color: #666;
z-index: 999;
border: 1px solid rgba(255,255,255,0.3);
font-weight: 500;
}
.badge {
background: linear-gradient(135deg, #e9ecef, #dee2e6);
color: #495057;
padding: 6px 12px;
border-radius: 15px;
font-size: 0.8rem;
margin-right: 8px;
font-weight: 500;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.badge-primary {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
}
.badge-success {
background: linear-gradient(135deg, #27ae60, #229954);
color: white;
}
.badge-warning {
background: linear-gradient(135deg, #f39c12, #e67e22);
color: white;
}
.card {
background: rgba(255,255,255,0.9);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 20px;
margin-bottom: 15px;
border: 1px solid rgba(255,255,255,0.3);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0,0,0,0.15);
}
.card-header {
font-weight: 600;
color: #2c3e50;
margin-bottom: 10px;
font-size: 1.1rem;
}
.card-body {
color: #666;
line-height: 1.6;
}
.notification-item {
padding: 15px;
border-bottom: 1px solid rgba(255,255,255,0.3);
transition: all 0.2s ease;
}
.notification-item:hover {
background: rgba(52, 152, 219, 0.05);
}
.notification-item:last-child {
border-bottom: none;
}
.notification-title {
font-weight: 600;
color: #2c3e50;
margin-bottom: 5px;
}
.notification-time {
font-size: 0.8rem;
color: #999;
}
.nav-section {
margin-bottom: 25px;
}
.nav-section-title {
font-size: 0.9rem;
font-weight: 600;
color: #666;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.nav-section .nav-link {
display: block;
padding: 12px 16px;
color: #666;
text-decoration: none;
border-radius: 8px;
margin-bottom: 5px;
transition: all 0.3s ease;
border-left: 3px solid transparent;
}
.nav-section .nav-link:hover {
background: rgba(52, 152, 219, 0.1);
color: #3498db;
border-left-color: #3498db;
transform: translateX(5px);
}
.divider {
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
margin: 20px 0;
}
.quick-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 25px;
}
.stat-card {
background: rgba(255,255,255,0.9);
backdrop-filter: blur(10px);
padding: 20px;
border-radius: 12px;
text-align: center;
border: 1px solid rgba(255,255,255,0.3);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
}
.stat-number {
font-size: 2rem;
font-weight: 700;
color: #3498db;
margin-bottom: 5px;
}
.stat-label {
font-size: 0.9rem;
color: #666;
font-weight: 500;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideIn {
from {
opacity: 0;
transform: scale(0.9) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@media (max-width: 768px) {
.container {
padding: 15px;
}
h1 {
font-size: 2rem;
}
.nav-list {
flex-direction: column;
}
.nav-link {
text-align: center;
}
.actions {
flex-direction: column;
}
.btn {
text-align: center;
}
.offcanvas {
width: 280px;
left: -280px;
}
.modal-content {
margin: 20px;
width: calc(100% - 40px);
}
.quick-stats {
grid-template-columns: 1fr;
}
.sub-nav {
justify-content: center;
}
}
@media (max-width: 480px) {
.content-area, .tab-content {
padding: 20px;
}
.modal-content {
padding: 25px;
}
.offcanvas {
width: 100%;
left: -100%;
}
}
</style>
</head>
<body>
<div class="status-indicator" id="statusIndicator">
Ready
</div>
<div class="container">
<header>
<h1>Minimal SPA Router</h1>
<p class="subtitle">Advanced single-page application with dynamic routing</p>
</header>
<!-- Main Navigation -->
<nav class="main-nav">
<ul class="nav-list" id="mainNav">
<li><a href="#tab/home" class="nav-link" data-tab="home">🏠 Home</a></li>
<li><a href="#tab/profile" class="nav-link" data-tab="profile">👤 Profile</a></li>
<li><a href="#tab/settings" class="nav-link" data-tab="settings">⚙️ Settings</a></li>
<li><a href="#tab/analytics" class="nav-link" data-tab="analytics">📊 Analytics</a></li>
<li><a href="#tab/projects" class="nav-link" data-tab="projects">📁 Projects</a></li>
</ul>
</nav>
<!-- Quick Stats -->
<div class="quick-stats">
<div class="stat-card">
<div class="stat-number">1,247</div>
<div class="stat-label">Active Users</div>
</div>
<div class="stat-card">
<div class="stat-number">89</div>
<div class="stat-label">Projects</div>
</div>
<div class="stat-card">
<div class="stat-number">156</div>
<div class="stat-label">Tasks Completed</div>
</div>
<div class="stat-card">
<div class="stat-number">98.5%</div>
<div class="stat-label">Uptime</div>
</div>
</div>
<!-- Page Content -->
<div id="pageContent" class="content-area">
<h2>Welcome to Your Dashboard</h2>
<p>This is a modern single-page application with advanced routing capabilities. Navigate through different sections using the tabs above or the action buttons below.</p>
<div class="divider"></div>
<h3>Recent Activity</h3>
<div class="card">
<div class="card-header">Project Alpha Update</div>
<div class="card-body">New features have been deployed to the production environment. All systems are running smoothly.</div>
</div>
<div class="card">
<div class="card-header">Team Meeting Scheduled</div>
<div class="card-body">Weekly team sync meeting scheduled for tomorrow at 2:00 PM. Please review the agenda beforehand.</div>
</div>
</div>
<!-- Tab Content -->
<div id="tabContent" class="tab-content">
<h3>Tab Content Area</h3>
<p>Select a tab from the navigation above to see dynamic content loaded here. Each tab provides different functionality and options.</p>
<div class="actions">
<button class="btn btn-primary">Primary Action</button>
<button class="btn btn-success">Success Action</button>
<button class="btn btn-warning">Warning Action</button>
<button class="btn btn-danger">Danger Action</button>
</div>
</div>
<!-- Main Actions -->
<div class="actions">
<button class="btn btn-primary" onclick="Router.navigate({ page: { name: 'dashboard', params: { view: 'analytics' } } })">
📊 Dashboard
</button>
<button class="btn btn-success" onclick="Router.navigate({ page: { name: 'projects', params: { status: 'active' } } })">
📁 Projects
</button>
<button class="btn" onclick="Router.navigate({ offcanvas: { name: 'menu' } })">
🍔 Menu
</button>
<button class="btn" onclick="Router.navigate({ offcanvas: { name: 'notifications' } })">
🔔 Notifications
</button>
<button class="btn" onclick="Router.navigate({ modal: { name: 'login' } })">
🔐 Login
</button>
<button class="btn" onclick="Router.navigate({ modal: { name: 'preferences' } })">
⚙️ Preferences
</button>
<button class="btn btn-warning" onclick="Router.clearAll()">
🗑️ Clear All
</button>
</div>
</div>
<!-- Offcanvas Menu -->
<div class="offcanvas-backdrop" id="offcanvasBackdrop" onclick="Router.navigate({ offcanvas: null })"></div>
<div class="offcanvas" id="offcanvas">
<div class="offcanvas-header">
<h4 class="offcanvas-title">Navigation</h4>
<button class="modal-close" onclick="Router.navigate({ offcanvas: null })">&times;</button>
</div>
<div id="offcanvasContent">
<div class="nav-section">
<div class="nav-section-title">Main Navigation</div>
<a href="#page/home" class="nav-link">🏠 Home</a>
<a href="#page/dashboard" class="nav-link">📊 Dashboard</a>
<a href="#page/projects" class="nav-link">📁 Projects</a>
<a href="#tab/profile" class="nav-link">👤 Profile</a>
<a href="#tab/settings" class="nav-link">⚙️ Settings</a>
</div>
<div class="divider"></div>
<div class="nav-section">
<div class="nav-section-title">Quick Actions</div>
<a href="#" onclick="Router.navigate({ modal: { name: 'newProject' } })" class="nav-link">➕ New Project</a>
<a href="#" onclick="Router.navigate({ modal: { name: 'addUser' } })" class="nav-link">👥 Add User</a>
<a href="#" onclick="Router.navigate({ modal: { name: 'generateReport' } })" class="nav-link">📈 Generate Report</a>
</div>
<div class="divider"></div>
<div class="actions">
<button class="btn btn-primary" onclick="Router.navigate({ modal: { name: 'quickSearch' } })">🔍 Quick Search</button>
<button class="btn" onclick="Router.navigate({ offcanvas: { name: 'notifications' } })">🔔 Notifications</button>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal" id="modal" onclick="closeModalOnBackdrop(event)">
<div class="modal-content">
<button class="modal-close" onclick="Router.navigate({ modal: null })">&times;</button>
<div id="modalContent">
<h3>Modal Title</h3>
<p>This is a sample modal with glassmorphism effects and modern styling.</p>
<div class="form-group">
<label class="form-label">Sample Input:</label>
<input type="text" class="form-control" placeholder="Enter some text...">
</div>
<div class="form-group">
<label class="form-label">Sample Select:</label>
<select class="form-control">
<option>Option 1</option>
<option>Option 2</option>
<option>Option 3</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Sample Textarea:</label>
<textarea class="form-control" rows="3" placeholder="Enter your message..."></textarea>
</div>
<div class="actions">
<button class="btn btn-primary">Save Changes</button>
<button class="btn" onclick="Router.navigate({ modal: null })">Cancel</button>
<button class="btn" onclick="Router.navigate({ offcanvas: { name: 'relatedActions' } })">Related Actions</button>
</div>
</div>
</div>
</div>
<!-- Additional Modal Examples (Hidden by default) -->
<div class="modal" id="loginModal">
<div class="modal-content">
<button class="modal-close">&times;</button>
<h3>🔐 Login</h3>
<form>
<div class="form-group">
<label class="form-label">Email Address:</label>
<input type="email" class="form-control" placeholder="Enter your email">
</div>
<div class="form-group">
<label class="form-label">Password:</label>
<input type="password" class="form-control" placeholder="Enter your password">
</div>
<div class="actions">
<button type="button" class="btn btn-primary">Sign In</button>
<button type="button" class="btn">Forgot Password?</button>
</div>
</form>
</div>
</div>
<div class="modal" id="preferencesModal">
<div class="modal-content">
<button class="modal-close">&times;</button>
<h3>⚙️ Preferences</h3>
<form>
<div class="form-group">
<label class="form-label">Theme:</label>
<select class="form-control">
<option>🌞 Light</option>
<option>🌙 Dark</option>
<option>🔄 Auto</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Language:</label>
<select class="form-control">
<option>🇺🇸 English</option>
<option>🇪🇸 Spanish</option>
<option>🇫🇷 French</option>
<option>🇩🇪 German</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Notifications:</label>
<select class="form-control">
<option>🔔 All Notifications</option>
<option>📧 Email Only</option>
<option>🔕 Disabled</option>
</select>
</div>
<div class="actions">
<button type="button" class="btn btn-primary">Save Preferences</button>
<button type="button" class="btn">Reset to Default</button>
</div>
</form>
</div>
</div>
<!-- Additional Offcanvas Examples (Hidden by default) -->
<div class="offcanvas" id="notificationsOffcanvas">
<div class="offcanvas-header">
<h4 class="offcanvas-title">🔔 Notifications</h4>
<button class="modal-close">&times;</button>
</div>
<div>
<div class="notification-item">
<div class="notification-title">New Project Created</div>
<div class="notification-time">2 minutes ago</div>
</div>
<div class="notification-item">
<div class="notification-title">Report Generated Successfully</div>
<div class="notification-time">1 hour ago</div>
</div>
<div class="notification-item">
<div class="notification-title">Team Member Added</div>
<div class="notification-time">3 hours ago</div>
</div>
<div class="notification-item">
<div class="notification-title">System Update Available</div>
<div class="notification-time">5 hours ago</div>
</div>
<div class="notification-item">
<div class="notification-title">Backup Completed</div>
<div class="notification-time">1 day ago</div>
</div>
<div class="notification-item">
<div class="notification-title">New User Registration</div>
<div class="notification-time">2 days ago</div>
</div>
<div class="actions" style="margin-top: 20px;">
<button class="btn btn-primary">Mark All Read</button>
<button class="btn">Clear Notifications</button>
</div>
</div>
</body>
</html>
// Page Handler
class PageHandler {
static load(page) {
const content = page?.name
? this.createContent(page)
: '<h2>Welcome</h2><p>Select a tab from the navigation above to get started.</p>';
document.getElementById('pageContent').innerHTML = content;
}
static createContent(page) {
const params = page.params ? Array.from(page.params.entries()) : [];
const paramsList = params.length > 0
? `<div class="mt-3">${params.map(([k, v]) => `<span class="badge">${k}: ${v}</span>`).join('')}</div>`
: '';
switch (page.name) {
case 'dashboard':
return `
<h2>Dashboard</h2>
<p>Analytics and reporting dashboard</p>
${paramsList}
<ul class="sub-nav">
<li><a href="#page/dashboard/subtab/analytics" class="nav-link" data-subtab="analytics">Analytics</a></li>
<li><a href="#page/dashboard/subtab/reports" class="nav-link" data-subtab="reports">Reports</a></li>
<li><a href="#page/dashboard/subtab/settings" class="nav-link" data-subtab="settings">Settings</a></li>
<li><a href="#page/dashboard/subtab/users" class="nav-link" data-subtab="users">Users</a></li>
</ul>
<div id="dashboardSubContent">
<p>Select a dashboard section above.</p>
</div>
`;
case 'projects':
return `
<h2>Projects</h2>
<p>Manage your projects and tasks</p>
${paramsList}
<ul class="sub-nav">
<li><a href="#page/projects/subtab/active" class="nav-link" data-subtab="active">Active</a></li>
<li><a href="#page/projects/subtab/completed" class="nav-link" data-subtab="completed">Completed</a></li>
<li><a href="#page/projects/subtab/archived" class="nav-link" data-subtab="archived">Archived</a></li>
</ul>
<div id="projectsSubContent">
<p>Select a project category above.</p>
</div>
`;
default:
return `
<h2>${page.name.charAt(0).toUpperCase() + page.name.slice(1)}</h2>
<p>Page content for ${page.name}</p>
${paramsList}
`;
}
}
}
// Tab Handler
class TabHandler {
static load(tab) {
// Update active state
document.querySelectorAll('#mainNav .nav-link').forEach(link => {
link.classList.remove('active');
});
const content = tab?.name
? this.createContent(tab)
: '<p>Select a tab from the navigation above.</p>';
document.getElementById('tabContent').innerHTML = content;
if (tab?.name) {
const activeLink = document.querySelector(`[data-tab="${tab.name}"]`);
if (activeLink) activeLink.classList.add('active');
}
}
static createContent(tab) {
const params = tab.params ? Array.from(tab.params.entries()) : [];
const paramsList = params.length > 0
? `<div>${params.map(([k, v]) => `<span class="badge">${k}: ${v}</span>`).join('')}</div>`
: '';
switch (tab.name) {
case 'home':
return `
<h3>Home Tab</h3>
<p>Welcome to your home dashboard.</p>
${paramsList}
<div class="actions">
<button class="btn" onclick="Router.navigate({ modal: { name: 'welcome' } })">Welcome Modal</button>
<button class="btn" onclick="Router.navigate({ offcanvas: { name: 'quickActions' } })">Quick Actions</button>
</div>
`;
case 'profile':
return `
<h3>Profile Tab</h3>
<p>Manage your profile settings and information.</p>
${paramsList}
<div class="actions">
<button class="btn" onclick="Router.navigate({ modal: { name: 'editProfile' } })">Edit Profile</button>
<button class="btn" onclick="Router.navigate({ modal: { name: 'changePassword' } })">Change Password</button>
<button class="btn" onclick="Router.navigate({ offcanvas: { name: 'profileSettings' } })">Profile Settings</button>
</div>
`;
case 'settings':
return `
<h3>Settings Tab</h3>
<p>Configure application settings and preferences.</p>
${paramsList}
<div class="actions">
<button class="btn" onclick="Router.navigate({ modal: { name: 'preferences' } })">Preferences</button>
<button class="btn" onclick="Router.navigate({ modal: { name: 'notifications' } })">Notifications</button>
<button class="btn" onclick="Router.navigate({ offcanvas: { name: 'advancedSettings' } })">Advanced</button>
</div>
`;
default:
return `
<h3>${tab.name.charAt(0).toUpperCase() + tab.name.slice(1)} Tab</h3>
<p>Content for the ${tab.name} tab.</p>
${paramsList}
`;
}
}
}
// SubTab Handler
class SubTabHandler {
static load(subtab) {
if (!subtab?.name) return;
// Update sub-navigation active state
document.querySelectorAll('.sub-nav .nav-link').forEach(link => {
link.classList.remove('active');
});
const activeLink = document.querySelector(`[data-subtab="${subtab.name}"]`);
if (activeLink) activeLink.classList.add('active');
// Update sub-content based on current page
const currentPage = Router.current.get('page');
if (currentPage?.name === 'dashboard') {
this.loadDashboardSubContent(subtab);
} else if (currentPage?.name === 'projects') {
this.loadProjectsSubContent(subtab);
}
}
static loadDashboardSubContent(subtab) {
const subContent = document.getElementById('dashboardSubContent');
if (!subContent) return;
const params = subtab.params ? Array.from(subtab.params.entries()) : [];
const paramsList = params.length > 0
? `<div>${params.map(([k, v]) => `<span class="badge">${k}: ${v}</span>`).join('')}</div>`
: '';
switch (subtab.name) {
case 'analytics':
subContent.innerHTML = `
<h4>Analytics</h4>
<p>View detailed analytics and metrics.</p>
${paramsList}
<div class="actions">
<button class="btn" onclick="Router.navigate({ modal: { name: 'chartSettings' } })">Chart Settings</button>
<button class="btn" onclick="Router.navigate({ offcanvas: { name: 'analyticsFilters' } })">Filters</button>
</div>
`;
break;
case 'reports':
subContent.innerHTML = `
<h4>Reports</h4>
<p>Generate and view comprehensive reports.</p>
${paramsList}
<div class="actions">
<button class="btn" onclick="Router.navigate({ modal: { name: 'generateReport' } })">Generate Report</button>
<button class="btn" onclick="Router.navigate({ offcanvas: { name: 'reportTemplates' } })">Templates</button>
</div>
`;
break;
case 'users':
subContent.innerHTML = `
<h4>Users</h4>
<p>Manage users and permissions.</p>
${paramsList}
<div class="actions">
<button class="btn" onclick="Router.navigate({ modal: { name: 'addUser' } })">Add User</button>
<button class="btn" onclick="Router.navigate({ offcanvas: { name: 'userFilters' } })">User Filters</button>
</div>
`;
break;
default:
subContent.innerHTML = `
<h4>${subtab.name.charAt(0).toUpperCase() + subtab.name.slice(1)}</h4>
<p>Content for ${subtab.name} section.</p>
${paramsList}
`;
}
}
static loadProjectsSubContent(subtab) {
const subContent = document.getElementById('projectsSubContent');
if (!subContent) return;
const params = subtab.params ? Array.from(subtab.params.entries()) : [];
const paramsList = params.length > 0
? `<div>${params.map(([k, v]) => `<span class="badge">${k}: ${v}</span>`).join('')}</div>`
: '';
switch (subtab.name) {
case 'active':
subContent.innerHTML = `
<h4>Active Projects</h4>
<p>Currently active projects and tasks.</p>
${paramsList}
<div class="actions">
<button class="btn" onclick="Router.navigate({ modal: { name: 'newProject' } })">New Project</button>
<button class="btn" onclick="Router.navigate({ offcanvas: { name: 'projectFilters' } })">Filters</button>
</div>
`;
break;
case 'completed':
subContent.innerHTML = `
<h4>Completed Projects</h4>
<p>Successfully completed projects archive.</p>
${paramsList}
<div class="actions">
<button class="btn" onclick="Router.navigate({ modal: { name: 'projectDetails' } })">View Details</button>
<button class="btn" onclick="Router.navigate({ offcanvas: { name: 'exportOptions' } })">Export</button>
</div>
`;
break;
default:
subContent.innerHTML = `
<h4>${subtab.name.charAt(0).toUpperCase() + subtab.name.slice(1)}</h4>
<p>Content for ${subtab.name} section.</p>
${paramsList}
`;
}
}
}
// Modal Handler
class ModalHandler {
static load(modal) {
const modalEl = document.getElementById('modal');
if (modal?.name) {
document.getElementById('modalContent').innerHTML = this.createContent(modal);
modalEl.classList.add('show');
} else {
modalEl.classList.remove('show');
}
}
static createContent(modal) {
const params = modal.params ? Array.from(modal.params.entries()) : [];
const paramsList = params.length > 0
? `<div>${params.map(([k, v]) => `<span class="badge">${k}: ${v}</span>`).join('')}</div>`
: '';
switch (modal.name) {
case 'login':
return `
<h3>Login</h3>
<form>
<div class="form-group">
<label class="form-label">Email:</label>
<input type="email" class="form-control" placeholder="Enter your email">
</div>
<div class="form-group">
<label class="form-label">Password:</label>
<input type="password" class="form-control" placeholder="Enter your password">
</div>
${paramsList}
<div class="actions">
<button type="button" class="btn btn-primary">Login</button>
<button type="button" class="btn" onclick="Router.navigate({ modal: { name: 'forgotPassword' } })">Forgot Password?</button>
</div>
</form>
`;
case 'editProfile':
return `
<h3>Edit Profile</h3>
<form>
<div class="form-group">
<label class="form-label">Name:</label>
<input type="text" class="form-control" placeholder="Enter your name">
</div>
<div class="form-group">
<label class="form-label">Bio:</label>
<textarea class="form-control" rows="3" placeholder="Tell us about yourself"></textarea>
</div>
${paramsList}
<div class="actions">
<button type="button" class="btn btn-primary">Save Changes</button>
<button type="button" class="btn" onclick="Router.navigate({ modal: null })">Cancel</button>
</div>
</form>
`;
case 'preferences':
return `
<h3>Preferences</h3>
<form>
<div class="form-group">
<label class="form-label">Theme:</label>
<select class="form-control">
<option>Light</option>
<option>Dark</option>
<option>Auto</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Language:</label>
<select class="form-control">
<option>English</option>
<option>Spanish</option>
<option>French</option>
</select>
</div>
${paramsList}
<div class="actions">
<button type="button" class="btn btn-primary">Save</button>
<button type="button" class="btn" onclick="Router.navigate({ offcanvas: { name: 'advancedPrefs' } })">Advanced</button>
</div>
</form>
`;
case 'generateReport':
return `
<h3>Generate Report</h3>
<form>
<div class="form-group">
<label class="form-label">Report Type:</label>
<select class="form-control">
<option>Weekly Summary</option>
<option>Monthly Analysis</option>
<option>Custom Range</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Format:</label>
<select class="form-control">
<option>PDF</option>
<option>Excel</option>
<option>CSV</option>
</select>
</div>
${paramsList}
<div class="actions">
<button type="button" class="btn btn-primary">Generate</button>
<button type="button" class="btn" onclick="Router.navigate({ offcanvas: { name: 'reportHistory' } })">History</button>
</div>
</form>
`;
default:
return `
<h3>${modal.name.charAt(0).toUpperCase() + modal.name.slice(1)}</h3>
<p>Modal content for ${modal.name}</p>
${paramsList}
<div class="actions">
<button type="button" class="btn" onclick="Router.navigate({ offcanvas: { name: 'relatedActions' } })">Related Actions</button>
</div>
`;
}
}
}
// Offcanvas Handler
class OffcanvasHandler {
static load(offcanvas) {
const offcanvasEl = document.getElementById('offcanvas');
const backdrop = document.getElementById('offcanvasBackdrop');
if (offcanvas?.name) {
document.getElementById('offcanvasContent').innerHTML = this.createContent(offcanvas);
offcanvasEl.classList.add('show');
backdrop.classList.add('show');
} else {
offcanvasEl.classList.remove('show');
backdrop.classList.remove('show');
}
}
static createContent(offcanvas) {
const params = offcanvas.params ? Array.from(offcanvas.params.entries()) : [];
const paramsList = params.length > 0
? `<div>${params.map(([k, v]) => `<div><strong>${k}:</strong> ${v}</div>`).join('')}</div>`
: '';
switch (offcanvas.name) {
case 'menu':
return `
<h4>Navigation Menu</h4>
<p>Quick navigation options</p>
${paramsList}
<hr>
<nav>
<a href="#page/home" class="nav-link">🏠 Home</a>
<a href="#page/dashboard" class="nav-link">📊 Dashboard</a>
<a href="#page/projects" class="nav-link">📁 Projects</a>
<a href="#tab/profile" class="nav-link">👤 Profile</a>
<a href="#tab/settings" class="nav-link">⚙️ Settings</a>
</nav>
<hr>
<div class="actions">
<button class="btn" onclick="Router.navigate({ modal: { name: 'quickSearch' } })">Quick Search</button>
<button class="btn" onclick="Router.navigate({ offcanvas: { name: 'notifications' } })">Notifications</button>
</div>
`;
case 'quickActions':
return `
<h4>Quick Actions</h4>
<p>Frequently used actions and shortcuts</p>
${paramsList}
<hr>
<div class="actions" style="flex-direction: column;">
<button class="btn" onclick="Router.navigate({ modal: { name: 'newProject' } })">➕ New Project</button>
<button class="btn" onclick="Router.navigate({ modal: { name: 'addUser' } })">👥 Add User</button>
<button class="btn" onclick="Router.navigate({ modal: { name: 'generateReport' } })">📈 Generate Report</button>
<button class="btn" onclick="Router.navigate({ offcanvas: { name: 'recentItems' } })">🕒 Recent Items</button>
</div>
`;
case 'profileSettings':
return `
<h4>Profile Settings</h4>
<p>Advanced profile configuration</p>
${paramsList}
<hr>
<div class="actions" style="flex-direction: column;">
<button class="btn" onclick="Router.navigate({ modal: { name: 'changePassword' } })">🔒 Change Password</button>
<button class="btn" onclick="Router.navigate({ modal: { name: 'privacy' } })">🛡️ Privacy Settings</button>
<button class="btn" onclick="Router.navigate({ modal: { name: 'notifications' } })">🔔 Notifications</button>
<button class="btn" onclick="Router.navigate({ offcanvas: { name: 'accountSettings' } })">⚙️ Account Settings</button>
</div>
`;
case 'analyticsFilters':
return `
<h4>Analytics Filters</h4>
<p>Filter and customize your analytics view</p>
${paramsList}
<hr>
<form>
<div class="form-group">
<label class="form-label">Date Range:</label>
<select class="form-control">
<option>Last 7 days</option>
<option>Last 30 days</option>
<option>Last 3 months</option>
<option>Custom range</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Metrics:</label>
<select class="form-control" multiple>
<option>Page Views</option>
<option>Unique Visitors</option>
<option>Conversion Rate</option>
<option>Revenue</option>
</select>
</div>
<div class="actions">
<button type="button" class="btn btn-primary">Apply Filters</button>
<button type="button" class="btn" onclick="Router.navigate({ modal: { name: 'saveFilter' } })">Save Filter</button>
</div>
</form>
`;
case 'notifications':
return `
<h4>Notifications</h4>
<p>Recent notifications and alerts</p>
${paramsList}
<hr>
<div style="max-height: 300px; overflow-y: auto;">
<div style="padding: 10px; border-bottom: 1px solid #eee;">
<strong>New Project Created</strong><br>
<small>2 minutes ago</small>
</div>
<div style="padding: 10px; border-bottom: 1px solid #eee;">
<strong>Report Generated</strong><br>
<small>1 hour ago</small>
</div>
<div style="padding: 10px; border-bottom: 1px solid #eee;">
<strong>User Added</strong><br>
<small>3 hours ago</small>
</div>
</div>
<hr>
<div class="actions">
<button class="btn" onclick="Router.navigate({ modal: { name: 'notificationSettings' } })">Settings</button>
<button class="btn">Mark All Read</button>
</div>
`;
default:
return `
<h4>${offcanvas.name.charAt(0).toUpperCase() + offcanvas.name.slice(1)}</h4>
<p>Offcanvas content for ${offcanvas.name}</p>
${paramsList}
<hr>
<div class="actions">
<button class="btn" onclick="Router.navigate({ modal: { name: 'details' } })">View Details</button>
<button class="btn" onclick="Router.navigate({ offcanvas: null })">Close</button>
</div>
`;
}
}
}
// Router - Now focused only on routing logic
class Router {
static current = new Map();
static previous = new Map();
static init() {
this.bindEvents();
this.handleRoute();
console.log('Router initialized');
}
static bindEvents() {
window.addEventListener('hashchange', () => this.handleRoute());
window.addEventListener('popstate', () => this.handleRoute());
// Handle navigation clicks
document.addEventListener('click', (e) => {
if (e.target.matches('[data-tab]')) {
e.preventDefault();
const tab = e.target.dataset.tab;
this.navigate({ tab: { name: tab } });
}
if (e.target.matches('[data-subtab]')) {
e.preventDefault();
const subtab = e.target.dataset.subtab;
const currentPage = this.current.get('page');
this.navigate({
page: currentPage,
subtab: { name: subtab }
});
}
});
}
static parseHash() {
const hash = location.hash.slice(1);
if (!hash) return new Map();
const segments = hash.split('/').filter(Boolean);
const route = new Map();
for (let i = 0; i < segments.length; i += 2) {
const type = segments[i];
const segment = segments[i + 1] || '';
const [name, ...paramParts] = segment.split(';');
if (name) {
const params = new Map();
paramParts.forEach(p => {
const [k, v] = p.split('=');
if (k && v !== undefined) {
params.set(k, decodeURIComponent(v));
}
});
route.set(type, { name, params });
}
}
return route;
}
static handleRoute() {
try {
const route = this.parseHash();
const prev = new Map(this.current);
this.previous = prev;
// Delegate to specific handlers
if (!this.routesEqual(route.get('page'), prev.get('page'))) {
PageHandler.load(route.get('page'));
}
if (!this.routesEqual(route.get('tab'), prev.get('tab'))) {
TabHandler.load(route.get('tab'));
}
if (!this.routesEqual(route.get('subtab'), prev.get('subtab'))) {
SubTabHandler.load(route.get('subtab'));
}
if (!this.routesEqual(route.get('offcanvas'), prev.get('offcanvas'))) {
OffcanvasHandler.load(route.get('offcanvas'));
}
if (!this.routesEqual(route.get('modal'), prev.get('modal'))) {
ModalHandler.load(route.get('modal'));
}
this.current = route;
this.updateStatus();
} catch (error) {
console.error('Route handling error:', error);
}
}
static navigate(layers) {
try {
const newRoute = new Map(this.current);
Object.entries(layers).forEach(([type, data]) => {
if (data === null || data === undefined) {
newRoute.delete(type);
} else if (data.name) {
newRoute.set(type, {
name: data.name,
params: new Map(Object.entries(data.params || {}))
});
}
});
const newHash = this.buildHash(newRoute);
if (location.hash === newHash) {
this.handleRoute();
} else {
location.hash = newHash;
}
} catch (error) {
console.error('Navigation error:', error);
}
}
static buildHash(route) {
const parts = [];
for (const [type, data] of route) {
if (data?.name) {
const paramStr = Array.from(data.params || [])
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join(';');
parts.push(type, `${data.name}${paramStr ? ';' + paramStr : ''}`);
}
}
return '#' + parts.join('/');
}
static routesEqual(route1, route2) {
if (!route1 && !route2) return true;
if (!route1 || !route2) return false;
if (route1.name !== route2.name) return false;
const params1 = route1.params || new Map();
const params2 = route2.params || new Map();
if (params1.size !== params2.size) return false;
for (const [key, value] of params1) {
if (params2.get(key) !== value) return false;
}
return true;
}
static updateStatus() {
const activeRoutes = Array.from(this.current.keys());
document.getElementById('statusIndicator').textContent =
activeRoutes.length > 0 ? `Active: ${activeRoutes.join(', ')}` : 'Ready';
}
static clearAll() {
this.navigate({
page: null,
tab: null,
subtab: null,
modal: null,
offcanvas: null
});
}
}
// Close modal when clicking backdrop
function closeModalOnBackdrop(event) {
if (event.target === event.currentTarget) {
Router.navigate({ modal: null });
}
}
// Initialize router
document.addEventListener('DOMContentLoaded', () => {
Router.init();
});
// Global access
window.Router = Router;
window.PageHandler = PageHandler;
window.TabHandler = TabHandler;
window.SubTabHandler = SubTabHandler;
window.ModalHandler = ModalHandler;
window.OffcanvasHandler = OffcanvasHandler;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment