<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Airtable Bases - Workspace Overview</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
color: white;
margin-bottom: 40px;
padding: 20px;
}
.header h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.header p {
font-size: 1.1rem;
opacity: 0.9;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
text-align: center;
}
.stat-card .number {
font-size: 2.5rem;
font-weight: bold;
color: #667eea;
margin-bottom: 5px;
}
.stat-card .label {
color: #666;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.controls {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 30px;
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
}
.search-box {
flex: 1;
min-width: 200px;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.3s;
}
.search-box:focus {
outline: none;
border-color: #667eea;
}
.filter-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.filter-btn {
padding: 10px 20px;
border: 2px solid #667eea;
background: white;
color: #667eea;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.3s;
}
.filter-btn:hover {
background: #667eea;
color: white;
}
.filter-btn.active {
background: #667eea;
color: white;
}
.loading {
text-align: center;
padding: 60px 20px;
color: white;
font-size: 1.2rem;
}
.spinner {
border: 4px solid rgba(255,255,255,0.3);
border-top: 4px solid white;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
background: #ff6b6b;
color: white;
padding: 20px;
border-radius: 12px;
text-align: center;
margin: 20px 0;
}
.bases-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.base-card {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
transition: transform 0.3s, box-shadow 0.3s;
cursor: pointer;
position: relative;
overflow: hidden;
}
.base-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
}
.base-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: #667eea;
}
.base-name {
font-size: 1.3rem;
font-weight: 600;
color: #333;
margin-bottom: 12px;
line-height: 1.4;
}
.base-id {
font-family: 'Courier New', monospace;
font-size: 0.85rem;
color: #666;
margin-bottom: 12px;
word-break: break-all;
}
.permission-badge {
display: inline-block;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
text-transform: capitalize;
}
.permission-badge.create {
background: #d4edda;
color: #155724;
}
.permission-badge.comment {
background: #fff3cd;
color: #856404;
}
.permission-badge.read {
background: #d1ecf1;
color: #0c5460;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: white;
font-size: 1.1rem;
}
.footer {
text-align: center;
color: white;
margin-top: 40px;
padding: 20px;
opacity: 0.8;
}
/* Modal Styles */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 1000;
backdrop-filter: blur(4px);
animation: fadeIn 0.3s;
}
.modal-overlay.show {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
transform: translateY(50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.modal {
background: white;
border-radius: 16px;
max-width: 800px;
width: 100%;
max-height: 90vh;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
animation: slideUp 0.3s;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 24px;
border-bottom: 2px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.modal-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
}
.modal-close {
background: rgba(255,255,255,0.2);
border: none;
color: white;
width: 36px;
height: 36px;
border-radius: 50%;
cursor: pointer;
font-size: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s;
}
.modal-close:hover {
background: rgba(255,255,255,0.3);
}
.modal-body {
padding: 24px;
overflow-y: auto;
flex: 1;
}
.modal-loading {
text-align: center;
padding: 40px;
color: #666;
}
.modal-loading .spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
.tables-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
}
.table-card {
background: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 16px;
transition: all 0.3s;
cursor: pointer;
}
.table-card:hover {
border-color: #667eea;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
}
.table-name {
font-size: 1.1rem;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.table-id {
font-family: 'Courier New', monospace;
font-size: 0.75rem;
color: #666;
margin-bottom: 8px;
word-break: break-all;
}
.table-info {
display: flex;
gap: 12px;
font-size: 0.85rem;
color: #666;
margin-top: 8px;
}
.table-info span {
display: flex;
align-items: center;
gap: 4px;
}
.table-description {
font-size: 0.9rem;
color: #666;
margin-top: 8px;
font-style: italic;
}
.base-info {
margin-bottom: 20px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
}
.base-info-item {
margin: 8px 0;
font-size: 0.9rem;
color: #666;
}
.base-info-item strong {
color: #333;
}
/* Records View Styles - Full Screen */
.records-view-fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
background: white;
z-index: 2000;
overflow: hidden;
}
.records-sidebar {
width: 350px;
border-right: 2px solid #e9ecef;
display: flex;
flex-direction: column;
background: #f8f9fa;
}
/* Comment Sidebar Styles */
.comments-sidebar {
width: 350px;
border-left: 2px solid #e9ecef;
display: flex;
flex-direction: column;
background: #f8f9fa;
overflow: hidden;
}
.comments-sidebar-header {
padding: 20px;
background: white;
border-bottom: 2px solid #e9ecef;
}
.comments-sidebar-header h3 {
margin: 0 0 15px 0;
font-size: 1.2rem;
color: #333;
}
.comments-list {
flex: 1;
overflow-y: auto;
padding: 15px;
}
.comment-item {
background: white;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
border: 1px solid #e9ecef;
}
.comment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.comment-author {
font-weight: 600;
color: #667eea;
font-size: 0.9rem;
}
.comment-time {
font-size: 0.75rem;
color: #999;
}
.comment-text {
color: #333;
font-size: 0.9rem;
line-height: 1.5;
margin-bottom: 8px;
word-wrap: break-word;
}
.comment-actions {
display: flex;
gap: 10px;
}
.comment-reply-btn {
background: none;
border: none;
color: #667eea;
cursor: pointer;
font-size: 0.8rem;
padding: 4px 8px;
border-radius: 4px;
transition: background 0.2s;
}
.comment-reply-btn:hover {
background: #f0f0f0;
}
.comment-reply-form {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #e9ecef;
}
.comment-input {
width: 100%;
padding: 8px 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 0.9rem;
resize: vertical;
min-height: 60px;
font-family: inherit;
}
.comment-input:focus {
outline: none;
border-color: #667eea;
}
.comment-submit-btn {
background: #667eea;
color: white;
border: none;
border-radius: 6px;
padding: 8px 16px;
cursor: pointer;
font-size: 0.9rem;
margin-top: 8px;
transition: background 0.2s;
}
.comment-submit-btn:hover {
background: #5568d3;
}
.comment-submit-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.comments-empty {
text-align: center;
padding: 40px 20px;
color: #999;
}
.comments-empty-icon {
font-size: 3rem;
margin-bottom: 10px;
}
.new-comment-form {
padding: 15px;
background: white;
border-top: 2px solid #e9ecef;
}
.new-comment-form h4 {
margin: 0 0 10px 0;
font-size: 1rem;
color: #333;
}
.back-btn {
background: #667eea;
color: white;
border: none;
border-radius: 6px;
padding: 8px 16px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.2s;
}
.back-btn:hover {
background: #5568d3;
transform: translateX(-2px);
}
.records-sidebar-header {
padding: 20px;
background: white;
border-bottom: 2px solid #e9ecef;
}
.records-sidebar-header h3 {
margin: 0 0 15px 0;
font-size: 1.2rem;
color: #333;
}
.records-search {
position: relative;
}
.records-search input {
width: 100%;
padding: 10px 35px 10px 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 0.9rem;
}
.records-search-icon {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
color: #666;
pointer-events: none;
}
.records-list {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.record-item {
background: white;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.2s;
}
.record-item:hover {
border-color: #667eea;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15);
}
.record-item.selected {
border-color: #667eea;
background: #f0f4ff;
}
.record-id {
font-family: 'Courier New', monospace;
font-size: 0.75rem;
color: #666;
margin-bottom: 8px;
word-break: break-all;
}
.record-preview {
font-size: 0.85rem;
color: #333;
margin-top: 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.record-date {
font-size: 0.75rem;
color: #999;
margin-top: 6px;
}
.records-detail {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.records-detail-header {
padding: 20px 24px;
border-bottom: 2px solid #e9ecef;
background: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.records-detail-header h2 {
margin: 0;
font-size: 1.3rem;
color: #333;
font-family: 'Courier New', monospace;
}
.records-detail-actions {
display: flex;
gap: 10px;
}
.detail-action-btn {
background: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 6px;
padding: 8px 12px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
.detail-action-btn:hover {
background: #e9ecef;
}
.records-detail-body {
flex: 1;
overflow-y: auto;
padding: 24px;
background: #fafbfc;
}
.record-summary {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 24px;
border: 2px solid #e9ecef;
}
.record-summary-item {
margin: 12px 0;
font-size: 0.9rem;
}
.record-summary-item strong {
color: #333;
display: inline-block;
min-width: 120px;
}
.record-fields {
background: white;
border-radius: 8px;
padding: 20px;
border: 2px solid #e9ecef;
}
.record-fields h3 {
margin: 0 0 20px 0;
font-size: 1.1rem;
color: #333;
padding-bottom: 12px;
border-bottom: 2px solid #e9ecef;
}
.field-item {
margin: 16px 0;
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
border-left: 4px solid #667eea;
}
.field-label {
font-weight: 600;
color: #333;
margin-bottom: 6px;
font-size: 0.9rem;
}
.field-value {
color: #666;
font-size: 0.9rem;
word-break: break-word;
}
.field-value pre {
margin: 0;
white-space: pre-wrap;
font-family: inherit;
}
.empty-records {
text-align: center;
padding: 60px 20px;
color: #666;
}
/* View Selector Styles */
.view-selector-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 1500;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
}
.view-selector-modal {
background: white;
border-radius: 16px;
padding: 30px;
max-width: 600px;
width: 90%;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.view-selector-header {
margin-bottom: 24px;
}
.view-selector-header h2 {
margin: 0 0 8px 0;
font-size: 1.5rem;
color: #333;
}
.view-selector-header p {
margin: 0;
color: #666;
font-size: 0.9rem;
}
.view-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
}
.view-option {
border: 2px solid #e9ecef;
border-radius: 12px;
padding: 24px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
background: white;
}
.view-option:hover {
border-color: #667eea;
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.2);
}
.view-option-icon {
font-size: 2.5rem;
margin-bottom: 12px;
}
.view-option-title {
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.view-option-desc {
font-size: 0.85rem;
color: #666;
}
/* List View Styles */
.list-view-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: white;
z-index: 2000;
display: flex;
flex-direction: column;
overflow: hidden;
}
.list-view-main {
display: flex;
flex: 1;
overflow: hidden;
}
.list-view-content-wrapper {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.list-view-header {
padding: 20px 24px;
border-bottom: 2px solid #e9ecef;
background: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.list-view-header-left {
display: flex;
align-items: center;
gap: 15px;
}
.list-view-content {
flex: 1;
overflow: auto;
padding: 20px;
}
.list-table {
width: 100%;
border-collapse: collapse;
background: white;
}
.list-table th {
background: #f8f9fa;
padding: 12px;
text-align: left;
font-weight: 600;
color: #333;
border-bottom: 2px solid #e9ecef;
position: sticky;
top: 0;
z-index: 10;
}
.list-table td {
padding: 12px;
border-bottom: 1px solid #f0f0f0;
color: #666;
}
.list-table tr:hover {
background: #f8f9fa;
}
/* Grid View Styles */
.grid-view-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #fafbfc;
z-index: 2000;
display: flex;
flex-direction: column;
overflow: hidden;
}
.grid-view-main {
display: flex;
flex: 1;
overflow: hidden;
}
.grid-view-content-wrapper {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.grid-view-header {
padding: 20px 24px;
border-bottom: 2px solid #e9ecef;
background: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.grid-view-content {
flex: 1;
overflow: auto;
padding: 24px;
}
.grid-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.grid-card {
background: white;
border: 2px solid #e9ecef;
border-radius: 12px;
padding: 20px;
transition: all 0.3s;
cursor: pointer;
}
.grid-card:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
transform: translateY(-2px);
}
.grid-card-header {
font-weight: 600;
color: #333;
margin-bottom: 12px;
font-size: 0.9rem;
font-family: 'Courier New', monospace;
}
.grid-card-fields {
display: flex;
flex-direction: column;
gap: 8px;
}
.grid-card-field {
display: flex;
flex-direction: column;
gap: 4px;
}
.grid-card-field-label {
font-size: 0.75rem;
color: #999;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.grid-card-field-value {
font-size: 0.9rem;
color: #333;
word-break: break-word;
}
@media (max-width: 768px) {
.header h1 {
font-size: 2rem;
}
.bases-grid {
grid-template-columns: 1fr;
}
.controls {
flex-direction: column;
}
.search-box {
width: 100%;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>π Airtable Workspace</h1>
<p>All your accessible bases in one place</p>
</div>
<div id="stats" class="stats" style="display: none;">
<div class="stat-card">
<div class="number" id="total-bases">0</div>
<div class="label">Total Bases</div>
</div>
<div class="stat-card">
<div class="number" id="create-permission">0</div>
<div class="label">Create Permission</div>
</div>
<div class="stat-card">
<div class="number" id="other-permission">0</div>
<div class="label">Other Permissions</div>
</div>
</div>
<div class="controls" style="display: none;" id="controls">
<input
type="text"
id="search-input"
class="search-box"
placeholder="Search bases by name..."
>
<div class="filter-buttons">
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="create">Create</button>
<button class="filter-btn" data-filter="comment">Comment</button>
<button class="filter-btn" data-filter="read">Read</button>
</div>
</div>
<div id="loading" class="loading">
<div class="spinner"></div>
<p>Loading your Airtable bases...</p>
</div>
<div id="error" class="error" style="display: none;"></div>
<div id="bases-container" class="bases-grid" style="display: none;"></div>
<div id="empty-state" class="empty-state" style="display: none;">
<p>No bases found matching your search criteria.</p>
</div>
<div class="footer">
<p>Powered by Airtable MCP Server</p>
</div>
</div>
<!-- Modal for displaying tables -->
<div id="modal-overlay" class="modal-overlay" onclick="closeModal(event)">
<div class="modal" onclick="event.stopPropagation()">
<div class="modal-header">
<h2 id="modal-title">Tables</h2>
<button class="modal-close" onclick="closeModal()">×</button>
</div>
<div class="modal-body" id="modal-body">
<div class="modal-loading">
<div class="spinner"></div>
<p>Loading tables...</p>
</div>
</div>
</div>
</div>
<!-- View Selector Modal -->
<div id="view-selector-overlay" class="view-selector-overlay" onclick="closeViewSelector(event)" style="display: none;">
<div class="view-selector-modal" onclick="event.stopPropagation()">
<div class="view-selector-header">
<h2 id="view-selector-table-name">Select View</h2>
<p id="view-selector-table-info">Choose how you want to view the records</p>
</div>
<div class="view-options">
<div class="view-option" onclick="openRecordReviewView()">
<div class="view-option-icon">π</div>
<div class="view-option-title">Record Review</div>
<div class="view-option-desc">Two-panel detailed view</div>
</div>
<div class="view-option" onclick="openListView()">
<div class="view-option-icon">π</div>
<div class="view-option-title">List View</div>
<div class="view-option-desc">Table/spreadsheet format</div>
</div>
<div class="view-option" onclick="openGridView()">
<div class="view-option-icon">π΄</div>
<div class="view-option-title">Grid View</div>
<div class="view-option-desc">Card-based layout</div>
</div>
</div>
</div>
</div>
<!-- Full-screen records view (Record Review) -->
<div id="records-view-container" class="records-view-fullscreen" style="display: none;">
<div class="records-sidebar">
<div class="records-sidebar-header">
<div style="display: flex; align-items: center; gap: 15px; margin-bottom: 15px;">
<button class="back-btn" onclick="closeRecordsView()" title="Back to tables">β Back</button>
<h3 id="records-table-name" style="margin: 0;">Table Records</h3>
</div>
<div class="records-search">
<input type="text" id="records-search-input" placeholder="Search..." onkeyup="filterRecords()">
<span class="records-search-icon">π</span>
</div>
</div>
<div class="records-list" id="records-list">
<div class="modal-loading">
<div class="spinner"></div>
<p>Loading records...</p>
</div>
</div>
</div>
<div class="records-detail">
<div class="records-detail-header">
<h2 id="selected-record-id">Select a record</h2>
<div class="records-detail-actions">
<button class="detail-action-btn" onclick="copyRecordId()">Copy ID</button>
</div>
</div>
<div class="records-detail-body" id="records-detail-body">
<div class="empty-records">
<p>Select a record from the list to view details</p>
</div>
</div>
</div>
<!-- Comment Sidebar -->
<div id="records-comments-sidebar" class="comments-sidebar" style="display: none;">
<div class="comments-sidebar-header">
<h3>π¬ Comments</h3>
</div>
<div class="comments-list" id="records-comments-list">
<div class="comments-empty">
<div class="comments-empty-icon">π</div>
<p>Select a record to view comments</p>
</div>
</div>
<div class="new-comment-form" id="records-new-comment-form" style="display: none;">
<h4>Add Comment</h4>
<textarea class="comment-input" id="records-comment-input" placeholder="Write a comment..."></textarea>
<button class="comment-submit-btn" onclick="submitComment('records')">Post Comment</button>
</div>
</div>
</div>
<!-- List View (Table/Spreadsheet) -->
<div id="list-view-container" class="list-view-container" style="display: none;">
<div class="list-view-header">
<div class="list-view-header-left">
<button class="back-btn" onclick="closeListView()">β Back</button>
<div>
<h2 id="list-view-table-name" style="margin: 0; font-size: 1.2rem;">Table Records</h2>
<p style="margin: 4px 0 0 0; font-size: 0.85rem; color: #666;" id="list-view-record-count">0 records</p>
</div>
</div>
<div style="display: flex; gap: 10px; align-items: center;">
<input type="text" id="list-view-search" placeholder="Search..." style="padding: 8px 12px; border: 2px solid #e0e0e0; border-radius: 6px; font-size: 0.9rem;" onkeyup="filterListView()">
<button class="detail-action-btn" onclick="switchView('selector')">Switch View</button>
</div>
</div>
<div class="list-view-main">
<div class="list-view-content-wrapper">
<div class="list-view-content">
<div id="list-view-table-wrapper" style="overflow-x: auto;">
<table class="list-table" id="list-view-table">
<thead id="list-view-thead"></thead>
<tbody id="list-view-tbody"></tbody>
</table>
</div>
</div>
</div>
<!-- Comment Sidebar -->
<div id="list-comments-sidebar" class="comments-sidebar" style="display: none;">
<div class="comments-sidebar-header">
<h3>π¬ Comments</h3>
</div>
<div class="comments-list" id="list-comments-list">
<div class="comments-empty">
<div class="comments-empty-icon">π</div>
<p>Click a record row to view comments</p>
</div>
</div>
<div class="new-comment-form" id="list-new-comment-form" style="display: none;">
<h4>Add Comment</h4>
<textarea class="comment-input" id="list-comment-input" placeholder="Write a comment..."></textarea>
<button class="comment-submit-btn" onclick="submitComment('list')">Post Comment</button>
</div>
</div>
</div>
</div>
<!-- Grid View (Card-based) -->
<div id="grid-view-container" class="grid-view-container" style="display: none;">
<div class="grid-view-header">
<div class="list-view-header-left">
<button class="back-btn" onclick="closeGridView()">β Back</button>
<div>
<h2 id="grid-view-table-name" style="margin: 0; font-size: 1.2rem;">Table Records</h2>
<p style="margin: 4px 0 0 0; font-size: 0.85rem; color: #666;" id="grid-view-record-count">0 records</p>
</div>
</div>
<div style="display: flex; gap: 10px; align-items: center;">
<input type="text" id="grid-view-search" placeholder="Search..." style="padding: 8px 12px; border: 2px solid #e0e0e0; border-radius: 6px; font-size: 0.9rem;" onkeyup="filterGridView()">
<button class="detail-action-btn" onclick="switchView('selector')">Switch View</button>
</div>
</div>
<div class="grid-view-main">
<div class="grid-view-content-wrapper">
<div class="grid-view-content">
<div class="grid-cards" id="grid-view-cards"></div>
</div>
</div>
<!-- Comment Sidebar -->
<div id="grid-comments-sidebar" class="comments-sidebar" style="display: none;">
<div class="comments-sidebar-header">
<h3>π¬ Comments</h3>
</div>
<div class="comments-list" id="grid-comments-list">
<div class="comments-empty">
<div class="comments-empty-icon">π</div>
<p>Click a card to view comments</p>
</div>
</div>
<div class="new-comment-form" id="grid-new-comment-form" style="display: none;">
<h4>Add Comment</h4>
<textarea class="comment-input" id="grid-comment-input" placeholder="Write a comment..."></textarea>
<button class="comment-submit-btn" onclick="submitComment('grid')">Post Comment</button>
</div>
</div>
</div>
</div>
<script>
let allBases = [];
let filteredBases = [];
// Fetch bases from API
async function fetchBases() {
try {
const response = await fetch('/api/bases');
const data = await response.json();
if (data.success) {
allBases = data.bases;
filteredBases = allBases;
renderBases();
updateStats();
document.getElementById('loading').style.display = 'none';
document.getElementById('stats').style.display = 'grid';
document.getElementById('controls').style.display = 'flex';
document.getElementById('bases-container').style.display = 'grid';
} else {
showError(data.error || 'Failed to fetch bases');
}
} catch (error) {
showError('Error connecting to server: ' + error.message);
}
}
function showError(message) {
document.getElementById('loading').style.display = 'none';
document.getElementById('error').style.display = 'block';
document.getElementById('error').textContent = 'β ' + message;
}
function updateStats() {
const total = allBases.length;
const create = allBases.filter(b => b.permissionLevel === 'create').length;
const other = total - create;
document.getElementById('total-bases').textContent = total;
document.getElementById('create-permission').textContent = create;
document.getElementById('other-permission').textContent = other;
}
function renderBases() {
const container = document.getElementById('bases-container');
if (filteredBases.length === 0) {
container.style.display = 'none';
document.getElementById('empty-state').style.display = 'block';
return;
}
document.getElementById('empty-state').style.display = 'none';
container.style.display = 'grid';
container.innerHTML = filteredBases.map(base => `
<div class="base-card" onclick="viewBaseTables('${base.id}', '${escapeHtml(base.name)}', '${base.permissionLevel}')">
<div class="base-name">${escapeHtml(base.name)}</div>
<div class="base-id">${base.id}</div>
<span class="permission-badge ${base.permissionLevel}">
${base.permissionLevel}
</span>
</div>
`).join('');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Modal functions
function viewBaseTables(baseId, baseName, permissionLevel) {
const modal = document.getElementById('modal-overlay');
const modalTitle = document.getElementById('modal-title');
const modalBody = document.getElementById('modal-body');
modalTitle.textContent = `π Tables in: ${baseName}`;
modalBody.innerHTML = `
<div class="base-info">
<div class="base-info-item"><strong>Base ID:</strong> ${baseId}</div>
<div class="base-info-item"><strong>Permission:</strong> <span class="permission-badge ${permissionLevel}">${permissionLevel}</span></div>
</div>
<div class="modal-loading">
<div class="spinner"></div>
<p>Loading tables...</p>
</div>
`;
modal.classList.add('show');
// Fetch tables
fetchTables(baseId);
}
async function fetchTables(baseId) {
try {
const response = await fetch(`/api/bases/${baseId}/tables`);
const data = await response.json();
const modalBody = document.getElementById('modal-body');
if (data.success) {
if (data.tables.length === 0) {
modalBody.innerHTML = `
<div class="base-info">
<div class="base-info-item"><strong>Base ID:</strong> ${baseId}</div>
</div>
<div class="empty-state" style="color: #666; padding: 40px;">
<p>No tables found in this base.</p>
</div>
`;
} else {
modalBody.innerHTML = `
<div class="base-info">
<div class="base-info-item"><strong>Base ID:</strong> ${baseId}</div>
<div class="base-info-item"><strong>Total Tables:</strong> ${data.tables.length}</div>
</div>
<div class="tables-grid">
${data.tables.map(table => `
<div class="table-card" onclick="showViewSelector('${baseId}', '${table.id}', '${escapeHtml(table.name)}', ${JSON.stringify(table).replace(/"/g, '"')})">
<div class="table-name">${escapeHtml(table.name)}</div>
<div class="table-id">${table.id}</div>
${table.description ? `<div class="table-description">${escapeHtml(table.description)}</div>` : ''}
<div class="table-info">
<span>π ${table.fields?.length || 0} fields</span>
<span>ποΈ ${table.views?.length || 0} views</span>
</div>
</div>
`).join('')}
</div>
`;
}
} else {
modalBody.innerHTML = `
<div class="error" style="margin: 20px 0;">
β Error: ${data.error || 'Failed to fetch tables'}
</div>
`;
}
} catch (error) {
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = `
<div class="error" style="margin: 20px 0;">
β Error loading tables: ${error.message}
</div>
`;
}
}
function closeModal(event) {
if (event && event.target !== event.currentTarget) {
return;
}
const modal = document.getElementById('modal-overlay');
modal.classList.remove('show');
}
function copyTableId(tableId, element) {
navigator.clipboard.writeText(tableId).then(() => {
// Visual feedback
const card = element;
const originalBg = card.style.background;
card.style.background = '#d4edda';
setTimeout(() => {
card.style.background = originalBg;
}, 500);
});
}
// Records view functions
let allRecords = [];
let filteredRecords = [];
let selectedRecord = null;
let currentBaseId = '';
let currentTableId = '';
let currentTableName = '';
let currentTableInfo = null;
let loadedTableId = ''; // Track which table's records are currently loaded
// View selector functions
async function showViewSelector(baseId, tableId, tableName, tableInfo) {
currentBaseId = baseId;
currentTableId = tableId;
currentTableName = tableName;
currentTableInfo = typeof tableInfo === 'string' ? JSON.parse(tableInfo.replace(/"/g, '"')) : tableInfo;
const overlay = document.getElementById('view-selector-overlay');
const tableNameEl = document.getElementById('view-selector-table-name');
const tableInfoEl = document.getElementById('view-selector-table-info');
tableNameEl.textContent = `View: ${tableName}`;
tableInfoEl.textContent = `Loading...`;
overlay.style.display = 'flex';
// Fetch records to get count
try {
await fetchRecords(baseId, tableId);
tableInfoEl.textContent = `${currentTableInfo?.fields?.length || 0} fields β’ ${allRecords.length} records`;
} catch (error) {
tableInfoEl.textContent = `${currentTableInfo?.fields?.length || 0} fields`;
}
}
function closeViewSelector(event) {
if (event && event.target !== event.currentTarget) {
return;
}
document.getElementById('view-selector-overlay').style.display = 'none';
}
function openRecordReviewView() {
closeViewSelector();
viewTableRecords(currentBaseId, currentTableId, currentTableName);
}
function viewTableRecords(baseId, tableId, tableName) {
currentBaseId = baseId;
currentTableId = tableId;
currentTableName = tableName;
selectedRecord = null;
// Hide main container
document.querySelector('.container').style.display = 'none';
const tableNameEl = document.getElementById('records-table-name');
const recordsList = document.getElementById('records-list');
const recordsViewContainer = document.getElementById('records-view-container');
tableNameEl.textContent = tableName;
recordsList.innerHTML = '<div class="modal-loading"><div class="spinner"></div><p>Loading records...</p></div>';
document.getElementById('records-detail-body').innerHTML = '<div class="empty-records"><p>Select a record from the list to view details</p></div>';
document.getElementById('selected-record-id').textContent = 'Select a record';
recordsViewContainer.style.display = 'flex';
// If records are already loaded, use them; otherwise fetch
if (allRecords.length > 0 && loadedTableId === tableId) {
filteredRecords = allRecords;
renderRecordsList();
if (allRecords.length > 0) {
selectRecord(allRecords[0]);
}
} else {
fetchRecords(baseId, tableId);
}
}
function openListView() {
closeViewSelector();
document.querySelector('.container').style.display = 'none';
const listView = document.getElementById('list-view-container');
listView.style.display = 'flex';
document.getElementById('list-view-table-name').textContent = currentTableName;
if (allRecords.length === 0) {
fetchRecords(currentBaseId, currentTableId).then(() => {
renderListView();
});
} else {
renderListView();
}
}
function openGridView() {
closeViewSelector();
document.querySelector('.container').style.display = 'none';
const gridView = document.getElementById('grid-view-container');
gridView.style.display = 'flex';
document.getElementById('grid-view-table-name').textContent = currentTableName;
if (allRecords.length === 0) {
fetchRecords(currentBaseId, currentTableId).then(() => {
renderGridView();
});
} else {
renderGridView();
}
}
function switchView(viewType) {
// Hide all views
document.getElementById('records-view-container').style.display = 'none';
document.getElementById('list-view-container').style.display = 'none';
document.getElementById('grid-view-container').style.display = 'none';
if (viewType === 'selector') {
showViewSelector(currentBaseId, currentTableId, currentTableName, currentTableInfo);
}
}
function closeListView() {
document.getElementById('list-view-container').style.display = 'none';
hideCommentsSidebar('list');
// Close tables modal if open
document.getElementById('modal-overlay').classList.remove('show');
// Show main bases view
document.querySelector('.container').style.display = 'block';
}
function closeGridView() {
document.getElementById('grid-view-container').style.display = 'none';
hideCommentsSidebar('grid');
// Close tables modal if open
document.getElementById('modal-overlay').classList.remove('show');
// Show main bases view
document.querySelector('.container').style.display = 'block';
}
function renderListView() {
const thead = document.getElementById('list-view-thead');
const tbody = document.getElementById('list-view-tbody');
const recordCount = document.getElementById('list-view-record-count');
if (filteredRecords.length === 0) {
tbody.innerHTML = '<tr><td colspan="100%" style="text-align: center; padding: 40px; color: #666;">No records found</td></tr>';
recordCount.textContent = '0 records';
return;
}
// Get all unique field names from all records
const allFieldNames = new Set();
filteredRecords.forEach(record => {
if (record.fields) {
Object.keys(record.fields).forEach(key => allFieldNames.add(key));
}
});
const fieldNames = Array.from(allFieldNames);
// Render header
thead.innerHTML = `
<tr>
<th style="width: 200px;">Record ID</th>
${fieldNames.map(field => `<th>${escapeHtml(field)}</th>`).join('')}
</tr>
`;
// Render rows
tbody.innerHTML = filteredRecords.map(record => {
const cells = fieldNames.map(field => {
const value = record.fields?.[field];
const displayValue = formatFieldValueForTable(value);
return `<td>${displayValue}</td>`;
}).join('');
return `
<tr onclick="showCommentsSidebar('list', '${record.id}')" style="cursor: pointer;">
<td style="font-family: 'Courier New', monospace; font-size: 0.85rem; color: #666;">${record.id}</td>
${cells}
</tr>
`;
}).join('');
recordCount.textContent = `${filteredRecords.length} record${filteredRecords.length !== 1 ? 's' : ''}`;
}
function renderGridView() {
const gridCards = document.getElementById('grid-view-cards');
const recordCount = document.getElementById('grid-view-record-count');
if (filteredRecords.length === 0) {
gridCards.innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 40px; color: #666;">No records found</div>';
recordCount.textContent = '0 records';
return;
}
gridCards.innerHTML = filteredRecords.map(record => {
const fields = record.fields || {};
const fieldEntries = Object.entries(fields).slice(0, 5); // Show first 5 fields
const fieldsHtml = fieldEntries.map(([key, value]) => {
const displayValue = formatFieldValueForGrid(value);
return `
<div class="grid-card-field">
<div class="grid-card-field-label">${escapeHtml(key)}</div>
<div class="grid-card-field-value">${displayValue}</div>
</div>
`;
}).join('');
const moreFields = Object.keys(fields).length > 5 ? `<div style="margin-top: 8px; font-size: 0.85rem; color: #999;">+${Object.keys(fields).length - 5} more fields</div>` : '';
return `
<div class="grid-card" onclick="showCommentsSidebar('grid', '${record.id}')">
<div class="grid-card-header">${record.id}</div>
<div class="grid-card-fields">
${fieldsHtml}
${moreFields}
</div>
</div>
`;
}).join('');
recordCount.textContent = `${filteredRecords.length} record${filteredRecords.length !== 1 ? 's' : ''}`;
}
function formatFieldValueForTable(value) {
if (value === null || value === undefined) {
return '<em style="color: #999;">null</em>';
}
if (Array.isArray(value)) {
return escapeHtml(value.slice(0, 3).join(', ') + (value.length > 3 ? '...' : ''));
}
if (typeof value === 'object') {
return '<span style="color: #999;">[Object]</span>';
}
const str = String(value);
return escapeHtml(str.length > 100 ? str.substring(0, 100) + '...' : str);
}
function formatFieldValueForGrid(value) {
if (value === null || value === undefined) {
return '<em style="color: #999;">null</em>';
}
if (Array.isArray(value)) {
return escapeHtml(value.slice(0, 2).join(', ') + (value.length > 2 ? '...' : ''));
}
if (typeof value === 'object') {
return '<span style="color: #999; font-size: 0.85rem;">[Object]</span>';
}
const str = String(value);
return escapeHtml(str.length > 150 ? str.substring(0, 150) + '...' : str);
}
function filterListView() {
const searchTerm = document.getElementById('list-view-search').value.toLowerCase();
filteredRecords = allRecords.filter(record => {
const recordId = record.id.toLowerCase();
const fields = record.fields ? Object.values(record.fields).map(v => String(v).toLowerCase()).join(' ') : '';
return recordId.includes(searchTerm) || fields.includes(searchTerm);
});
renderListView();
}
function filterGridView() {
const searchTerm = document.getElementById('grid-view-search').value.toLowerCase();
filteredRecords = allRecords.filter(record => {
const recordId = record.id.toLowerCase();
const fields = record.fields ? Object.values(record.fields).map(v => String(v).toLowerCase()).join(' ') : '';
return recordId.includes(searchTerm) || fields.includes(searchTerm);
});
renderGridView();
}
function copyRecordIdFromTable(recordId) {
navigator.clipboard.writeText(recordId).then(() => {
// Visual feedback could be added here
console.log('Copied:', recordId);
});
}
function closeRecordsView() {
const recordsViewContainer = document.getElementById('records-view-container');
recordsViewContainer.style.display = 'none';
hideCommentsSidebar('records');
// Close tables modal if open
document.getElementById('modal-overlay').classList.remove('show');
// Show main bases view
document.querySelector('.container').style.display = 'block';
}
async function fetchRecords(baseId, tableId) {
try {
const response = await fetch(`/api/bases/${baseId}/tables/${tableId}/records?maxRecords=100`);
const data = await response.json();
const recordsList = document.getElementById('records-list');
if (data.success) {
allRecords = data.records;
filteredRecords = allRecords;
loadedTableId = tableId; // Track which table we loaded
if (allRecords.length === 0) {
if (recordsList) {
recordsList.innerHTML = '<div class="empty-records"><p>No records found in this table.</p></div>';
}
} else {
if (recordsList) {
renderRecordsList();
if (allRecords.length > 0) {
selectRecord(allRecords[0]);
}
}
}
return data.records;
} else {
if (recordsList) {
recordsList.innerHTML = `<div class="error">β Error: ${data.error || 'Failed to fetch records'}</div>`;
}
throw new Error(data.error || 'Failed to fetch records');
}
} catch (error) {
const recordsList = document.getElementById('records-list');
if (recordsList) {
recordsList.innerHTML = `<div class="error">β Error loading records: ${error.message}</div>`;
}
throw error;
}
}
function renderRecordsList() {
const recordsList = document.getElementById('records-list');
if (filteredRecords.length === 0) {
recordsList.innerHTML = '<div class="empty-records"><p>No records match your search.</p></div>';
return;
}
recordsList.innerHTML = filteredRecords.map((record, index) => {
const preview = getRecordPreview(record);
const dateInfo = getRecordDateInfo(record);
return `
<div class="record-item ${selectedRecord?.id === record.id ? 'selected' : ''}"
onclick="selectRecordById('${record.id}')">
<div class="record-id">${record.id}</div>
${preview ? `<div class="record-preview">${escapeHtml(preview)}</div>` : ''}
${dateInfo ? `<div class="record-date">${dateInfo}</div>` : ''}
</div>
`;
}).join('');
}
function selectRecordById(recordId) {
const record = allRecords.find(r => r.id === recordId);
if (record) {
selectRecord(record);
}
}
function getRecordPreview(record) {
if (!record.fields) return null;
const fields = Object.values(record.fields);
const textFields = fields.filter(f => typeof f === 'string' && f.length < 200);
return textFields[0] || (fields[0] ? String(fields[0]).substring(0, 100) : null);
}
function getRecordDateInfo(record) {
if (!record.fields) return null;
const dateFields = Object.entries(record.fields).find(([key, value]) =>
value && (typeof value === 'string' && value.match(/\d{4}-\d{2}-\d{2}/))
);
if (dateFields) {
const date = new Date(dateFields[1]);
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) +
' ' + date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
}
return null;
}
function selectRecord(record) {
selectedRecord = record;
document.getElementById('selected-record-id').textContent = record.id;
renderRecordsList();
renderRecordDetails(record);
// Show comments sidebar and load comments
showCommentsSidebar('records', record.id);
}
// Comment handling functions
let currentCommentRecordId = null;
let currentCommentView = null;
async function showCommentsSidebar(viewType, recordId) {
currentCommentView = viewType;
currentCommentRecordId = recordId;
const sidebarId = `${viewType}-comments-sidebar`;
const listId = `${viewType}-comments-list`;
const formId = `${viewType}-new-comment-form`;
const inputId = `${viewType}-comment-input`;
const sidebar = document.getElementById(sidebarId);
const commentsList = document.getElementById(listId);
const commentForm = document.getElementById(formId);
const commentInput = document.getElementById(inputId);
// Show sidebar
sidebar.style.display = 'flex';
// Clear previous input
if (commentInput) {
commentInput.value = '';
}
// Show form
if (commentForm) {
commentForm.style.display = 'block';
}
// Load comments
commentsList.innerHTML = '<div class="modal-loading"><div class="spinner"></div><p>Loading comments...</p></div>';
await loadComments(viewType, recordId);
}
async function loadComments(viewType, recordId) {
if (!currentBaseId || !currentTableId || !recordId) {
return;
}
try {
const response = await fetch(`/api/bases/${currentBaseId}/tables/${currentTableId}/records/${recordId}/comments`);
const data = await response.json();
const listId = `${viewType}-comments-list`;
const commentsList = document.getElementById(listId);
if (data.success && data.comments && data.comments.length > 0) {
commentsList.innerHTML = data.comments.map(comment => renderComment(comment, viewType)).join('');
} else {
commentsList.innerHTML = `
<div class="comments-empty">
<div class="comments-empty-icon">π</div>
<p>No comments yet. Be the first to comment!</p>
</div>
`;
}
} catch (error) {
const listId = `${viewType}-comments-list`;
const commentsList = document.getElementById(listId);
commentsList.innerHTML = `
<div class="comments-empty">
<div class="comments-empty-icon">β οΈ</div>
<p>Error loading comments: ${error.message}</p>
</div>
`;
}
}
function renderComment(comment, viewType) {
const createdTime = comment.createdTime ? new Date(comment.createdTime).toLocaleString() : 'Unknown';
const author = comment.author?.name || comment.author?.email || 'Unknown';
const text = escapeHtml(comment.text || '');
return `
<div class="comment-item">
<div class="comment-header">
<span class="comment-author">${escapeHtml(author)}</span>
<span class="comment-time">${createdTime}</span>
</div>
<div class="comment-text">${text}</div>
</div>
`;
}
async function submitComment(viewType) {
if (!currentCommentRecordId || !currentBaseId || !currentTableId) {
alert('No record selected');
return;
}
const inputId = `${viewType}-comment-input`;
const commentInput = document.getElementById(inputId);
const submitBtn = commentInput.nextElementSibling;
if (!commentInput || !commentInput.value.trim()) {
alert('Please enter a comment');
return;
}
const text = commentInput.value.trim();
// Disable button
submitBtn.disabled = true;
submitBtn.textContent = 'Posting...';
try {
const response = await fetch(`/api/bases/${currentBaseId}/tables/${currentTableId}/records/${currentCommentRecordId}/comments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ text }),
});
const data = await response.json();
if (data.success) {
// Clear input
commentInput.value = '';
// Reload comments
await loadComments(viewType, currentCommentRecordId);
} else {
alert('Error posting comment: ' + (data.error || 'Unknown error'));
}
} catch (error) {
alert('Error posting comment: ' + error.message);
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Post Comment';
}
}
function hideCommentsSidebar(viewType) {
const sidebarId = `${viewType}-comments-sidebar`;
const sidebar = document.getElementById(sidebarId);
if (sidebar) {
sidebar.style.display = 'none';
}
currentCommentRecordId = null;
currentCommentView = null;
}
function renderRecordDetails(record) {
const detailBody = document.getElementById('records-detail-body');
if (!record.fields || Object.keys(record.fields).length === 0) {
detailBody.innerHTML = '<div class="empty-records"><p>This record has no fields.</p></div>';
return;
}
const fieldsHtml = Object.entries(record.fields).map(([key, value]) => {
const displayValue = formatFieldValue(value);
return `
<div class="field-item">
<div class="field-label">${escapeHtml(key)}</div>
<div class="field-value">${displayValue}</div>
</div>
`;
}).join('');
detailBody.innerHTML = `
<div class="record-summary">
<div class="record-summary-item">
<strong>Record ID:</strong> ${record.id}
</div>
<div class="record-summary-item">
<strong>Fields:</strong> ${Object.keys(record.fields).length}
</div>
</div>
<div class="record-fields">
<h3>Field Values</h3>
${fieldsHtml}
</div>
`;
}
function formatFieldValue(value) {
if (value === null || value === undefined) {
return '<em>null</em>';
}
if (Array.isArray(value)) {
return value.map(v => escapeHtml(String(v))).join(', ');
}
if (typeof value === 'object') {
return '<pre>' + escapeHtml(JSON.stringify(value, null, 2)) + '</pre>';
}
return escapeHtml(String(value));
}
function filterRecords() {
const searchTerm = document.getElementById('records-search-input').value.toLowerCase();
filteredRecords = allRecords.filter(record => {
const recordId = record.id.toLowerCase();
const fields = record.fields ? Object.values(record.fields).map(v => String(v).toLowerCase()).join(' ') : '';
return recordId.includes(searchTerm) || fields.includes(searchTerm);
});
renderRecordsList();
if (selectedRecord && filteredRecords.find(r => r.id === selectedRecord.id)) {
renderRecordDetails(selectedRecord);
} else if (filteredRecords.length > 0) {
selectRecord(filteredRecords[0]);
} else {
document.getElementById('records-detail-body').innerHTML = '<div class="empty-records"><p>No records match your search.</p></div>';
document.getElementById('selected-record-id').textContent = 'No record selected';
}
}
function closeRecordsModal(event) {
// This function is kept for compatibility but redirects to closeRecordsView
closeRecordsView();
}
function copyRecordId() {
if (selectedRecord) {
navigator.clipboard.writeText(selectedRecord.id).then(() => {
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = 'Copied!';
btn.style.background = '#d4edda';
setTimeout(() => {
btn.textContent = originalText;
btn.style.background = '';
}, 1000);
});
}
}
// Search functionality
document.getElementById('search-input').addEventListener('input', (e) => {
const searchTerm = e.target.value.toLowerCase();
filterBases(searchTerm, getActiveFilter());
});
// Filter buttons
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const filter = btn.dataset.filter;
filterBases(document.getElementById('search-input').value.toLowerCase(), filter);
});
});
function getActiveFilter() {
const activeBtn = document.querySelector('.filter-btn.active');
return activeBtn ? activeBtn.dataset.filter : 'all';
}
function filterBases(searchTerm, permissionFilter) {
filteredBases = allBases.filter(base => {
const matchesSearch = base.name.toLowerCase().includes(searchTerm) ||
base.id.toLowerCase().includes(searchTerm);
const matchesFilter = permissionFilter === 'all' ||
base.permissionLevel === permissionFilter;
return matchesSearch && matchesFilter;
});
renderBases();
}
// Initialize
fetchBases();
</script>
</body>
</html>