AppForge: migrate off Supabase to external Postgres + Better Auth + local storage
- New Express + TypeScript backend (server/) with pg, Better Auth, local file storage - De-Supabased Postgres schema (server/db) and TS reimplementations of DB functions - Frontend data layer rewired to REST (rest-client + backend-client compat shim) - Removed all Supabase references (code, config, deps, docs) - New brand assets: gradient favicon/app icons + dark/white wordmark logos Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+2937
File diff suppressed because it is too large
Load Diff
+372
@@ -0,0 +1,372 @@
|
||||
/**
|
||||
* AppForge Documentation v2.0.2
|
||||
* Enhanced interactive documentation with smooth animations and modern UX
|
||||
*/
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// THEME MANAGEMENT
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function toggleTheme() {
|
||||
const current = document.documentElement.getAttribute('data-theme');
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
document.documentElement.setAttribute('data-theme', next);
|
||||
localStorage.setItem('docs-theme', next);
|
||||
}
|
||||
|
||||
function initTheme() {
|
||||
const stored = localStorage.getItem('docs-theme');
|
||||
if (stored) {
|
||||
document.documentElement.setAttribute('data-theme', stored);
|
||||
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// SIDEBAR SEARCH / FILTER
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function filterSidebar() {
|
||||
const input = document.getElementById('sidebarSearch');
|
||||
if (!input) return;
|
||||
const query = input.value.toLowerCase().trim();
|
||||
const sections = document.querySelectorAll('.sidebar-section');
|
||||
sections.forEach(section => {
|
||||
const links = section.querySelectorAll('.sidebar-link');
|
||||
let anyVisible = false;
|
||||
links.forEach(link => {
|
||||
const text = link.textContent.toLowerCase();
|
||||
const match = !query || text.includes(query);
|
||||
link.parentElement.style.display = match ? '' : 'none';
|
||||
if (match) anyVisible = true;
|
||||
});
|
||||
const title = section.querySelector('.sidebar-title');
|
||||
if (title) {
|
||||
section.style.display = anyVisible || !query ? '' : 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// SIDEBAR MANAGEMENT
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function toggleSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
if (sidebar) sidebar.classList.toggle('open');
|
||||
if (overlay) overlay.classList.toggle('open');
|
||||
}
|
||||
|
||||
function closeSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
if (sidebar) sidebar.classList.remove('open');
|
||||
if (overlay) overlay.classList.remove('open');
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// COLLAPSIBLE SIDEBAR SECTIONS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function toggleSidebarSection(el) {
|
||||
const section = el.closest('.sidebar-section');
|
||||
if (section) section.classList.toggle('collapsed');
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// FAQ ACCORDION
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function toggleFAQ(element) {
|
||||
const answer = element.nextElementSibling;
|
||||
const arrow = element.querySelector('.faq-arrow');
|
||||
|
||||
if (answer) {
|
||||
answer.classList.toggle('open');
|
||||
}
|
||||
|
||||
if (arrow) {
|
||||
arrow.style.transform = answer && answer.classList.contains('open')
|
||||
? 'rotate(180deg)'
|
||||
: 'rotate(0)';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// FAQ SEARCH / FILTER
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function filterFAQs() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
if (!searchInput) return;
|
||||
|
||||
const query = searchInput.value.toLowerCase().trim();
|
||||
const faqItems = document.querySelectorAll('.faq-item');
|
||||
const faqSections = document.querySelectorAll('#help-center .card');
|
||||
|
||||
faqItems.forEach(item => {
|
||||
const text = item.textContent.toLowerCase();
|
||||
item.style.display = text.includes(query) ? '' : 'none';
|
||||
});
|
||||
|
||||
// Hide sections where all items are hidden
|
||||
faqSections.forEach(section => {
|
||||
const items = section.querySelectorAll('.faq-item');
|
||||
if (items.length === 0) return;
|
||||
const allHidden = Array.from(items).every(i => i.style.display === 'none');
|
||||
if (section.querySelector('.faq-list')) {
|
||||
section.style.display = allHidden ? 'none' : '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// SMOOTH SCROLL
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function scrollToSection(id) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// COPY CODE TO CLIPBOARD
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function copyCode(button) {
|
||||
const codeBlock = button.closest('.code-block');
|
||||
const code = codeBlock ? codeBlock.querySelector('code') : null;
|
||||
|
||||
if (code) {
|
||||
navigator.clipboard.writeText(code.textContent).then(() => {
|
||||
const originalText = button.textContent;
|
||||
button.textContent = '✓ Copied!';
|
||||
button.classList.add('copied');
|
||||
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
button.classList.remove('copied');
|
||||
}, 2000);
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// TAB NAVIGATION
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function switchTab(tabGroup, tabId) {
|
||||
const group = document.querySelector(`[data-tab-group="${tabGroup}"]`);
|
||||
if (!group) return;
|
||||
|
||||
group.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.tab === tabId);
|
||||
});
|
||||
|
||||
group.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.toggle('active', content.id === tabId);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// READING PROGRESS BAR
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function updateReadingProgress() {
|
||||
const bar = document.querySelector('.reading-progress');
|
||||
if (!bar) return;
|
||||
|
||||
const scrollTop = window.scrollY;
|
||||
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
||||
const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
|
||||
bar.style.width = Math.min(progress, 100) + '%';
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// ACTIVE SIDEBAR LINK (SCROLL SPY)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function updateActiveSidebarLink() {
|
||||
const sections = document.querySelectorAll('section[id], div[id]');
|
||||
const sidebarLinks = document.querySelectorAll('.sidebar-link, .sidebar-link-sub');
|
||||
const navLinks = document.querySelectorAll('.nav-center a');
|
||||
|
||||
let currentSection = '';
|
||||
const offset = 120;
|
||||
|
||||
sections.forEach(section => {
|
||||
const rect = section.getBoundingClientRect();
|
||||
if (rect.top <= offset) {
|
||||
currentSection = section.id;
|
||||
}
|
||||
});
|
||||
|
||||
// Update sidebar links
|
||||
sidebarLinks.forEach(link => {
|
||||
const href = link.getAttribute('href');
|
||||
if (href && href === '#' + currentSection) {
|
||||
link.classList.add('active');
|
||||
} else if (href && currentSection && href.includes('#')) {
|
||||
// Check if this is a parent section
|
||||
const section = document.getElementById(currentSection);
|
||||
const linkTarget = href.substring(1);
|
||||
if (section && section.closest('[id="' + linkTarget + '"]')) {
|
||||
link.classList.add('active');
|
||||
} else {
|
||||
link.classList.remove('active');
|
||||
}
|
||||
} else {
|
||||
link.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Update nav links
|
||||
const mainSections = ['getting-started', 'api-reference', 'native-features', 'deployment'];
|
||||
navLinks.forEach(link => {
|
||||
const href = link.getAttribute('href');
|
||||
if (!href) return;
|
||||
const target = href.substring(1);
|
||||
const isActive = mainSections.some(s => {
|
||||
if (target === s) {
|
||||
const el = document.getElementById(s);
|
||||
if (!el) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
return rect.top <= offset && rect.bottom > 0;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
link.classList.toggle('active', currentSection === target || isActive);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// BACK TO TOP BUTTON
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function updateBackToTop() {
|
||||
const btn = document.querySelector('.back-to-top');
|
||||
if (!btn) return;
|
||||
btn.classList.toggle('visible', window.scrollY > 400);
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// NAV SCROLL EFFECT
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function updateNavEffect() {
|
||||
const nav = document.querySelector('.nav');
|
||||
if (!nav) return;
|
||||
nav.classList.toggle('scrolled', window.scrollY > 10);
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// SCROLL ANIMATIONS (Intersection Observer)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function initScrollAnimations() {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('animate-in');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1, rootMargin: '0px 0px -40px 0px' }
|
||||
);
|
||||
|
||||
document.querySelectorAll('.card, .callout, .table-wrapper').forEach(el => {
|
||||
el.style.opacity = '0';
|
||||
el.style.transform = 'translateY(20px)';
|
||||
observer.observe(el);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// AUTO-WRAP CODE BLOCKS WITH COPY BUTTON
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function initCodeBlocks() {
|
||||
document.querySelectorAll('pre').forEach(pre => {
|
||||
// Skip if already wrapped
|
||||
if (pre.parentElement.classList.contains('code-block')) return;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'code-block';
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'copy-btn';
|
||||
btn.textContent = 'Copy';
|
||||
btn.onclick = function() { copyCode(this); };
|
||||
|
||||
pre.parentNode.insertBefore(wrapper, pre);
|
||||
wrapper.appendChild(pre);
|
||||
wrapper.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// DEBOUNCE UTILITY
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// INITIALIZATION
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Scroll handlers (debounced)
|
||||
const onScroll = () => {
|
||||
updateReadingProgress();
|
||||
updateActiveSidebarLink();
|
||||
updateBackToTop();
|
||||
updateNavEffect();
|
||||
};
|
||||
window.addEventListener('scroll', debounce(onScroll, 16), { passive: true });
|
||||
onScroll(); // Initial call
|
||||
|
||||
// Initialize code blocks with copy buttons
|
||||
initCodeBlocks();
|
||||
|
||||
// Initialize scroll animations
|
||||
initScrollAnimations();
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Ctrl/Cmd + K to focus search
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
const searchInput = document.getElementById('searchInput') || document.querySelector('.nav-search-input');
|
||||
if (searchInput) searchInput.focus();
|
||||
}
|
||||
// Escape to close sidebar
|
||||
if (e.key === 'Escape') {
|
||||
closeSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
// Close sidebar when clicking a link (mobile)
|
||||
document.querySelectorAll('.sidebar-link, .sidebar-link-sub').forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
if (window.innerWidth <= 900) closeSidebar();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Run theme initialization immediately (before DOM ready)
|
||||
initTheme();
|
||||
+1198
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user