- Implement PostgreSQL database schema with users and bookmarks tables - Add database connection pooling with retry logic and error handling - Create migration system with automatic schema initialization - Add database CLI tools for management (init, status, validate, etc.) - Include comprehensive error handling and diagnostics - Add development seed data and testing utilities - Implement health monitoring and connection pool statistics - Create detailed documentation and troubleshooting guide Database features: - Users table with authentication fields and email verification - Bookmarks table with user association and metadata - Proper indexes for performance optimization - Automatic timestamp triggers - Transaction support with rollback handling - Connection pooling (20 max connections, 30s idle timeout) - Graceful shutdown handling CLI commands available: - npm run db:init - Initialize database - npm run db:status - Check database status - npm run db:validate - Validate schema - npm run db:test - Run database tests - npm run db:diagnostics - Full diagnostics
589 lines
23 KiB
HTML
589 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Mobile Touch Interactions Test - Bookmark Manager</title>
|
|
<link rel="stylesheet" href="styles.css">
|
|
<style>
|
|
/* Test-specific styles */
|
|
.test-container {
|
|
max-width: 400px;
|
|
margin: 20px auto;
|
|
padding: 20px;
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.test-section {
|
|
margin-bottom: 30px;
|
|
padding: 20px;
|
|
border: 2px solid #e9ecef;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.test-section h3 {
|
|
margin-top: 0;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.test-instructions {
|
|
background: #f8f9fa;
|
|
padding: 15px;
|
|
border-radius: 6px;
|
|
margin-bottom: 15px;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.test-bookmark {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
margin: 10px 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
min-height: 60px;
|
|
touch-action: pan-x;
|
|
transition: transform 0.3s ease, background-color 0.3s ease;
|
|
position: relative;
|
|
}
|
|
|
|
.test-bookmark.swiping {
|
|
transform: translateX(var(--swipe-offset, 0));
|
|
transition: none;
|
|
}
|
|
|
|
.test-bookmark.swipe-left {
|
|
background-color: #dc3545;
|
|
color: white;
|
|
}
|
|
|
|
.test-bookmark.swipe-right {
|
|
background-color: #28a745;
|
|
color: white;
|
|
}
|
|
|
|
.test-bookmark::before,
|
|
.test-bookmark::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 24px;
|
|
height: 24px;
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
z-index: 1;
|
|
}
|
|
|
|
.test-bookmark::before {
|
|
left: 20px;
|
|
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" fill="white" viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>') no-repeat center;
|
|
background-size: contain;
|
|
}
|
|
|
|
.test-bookmark::after {
|
|
right: 20px;
|
|
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" fill="white" viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>') no-repeat center;
|
|
background-size: contain;
|
|
}
|
|
|
|
.test-bookmark.swipe-right::before {
|
|
opacity: 1;
|
|
}
|
|
|
|
.test-bookmark.swipe-left::after {
|
|
opacity: 1;
|
|
}
|
|
|
|
.test-bookmark-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.test-bookmark-title {
|
|
font-weight: 500;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.test-bookmark-url {
|
|
font-size: 14px;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.test-status {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
background: #6c757d;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.pull-refresh-area {
|
|
height: 200px;
|
|
border: 2px dashed #dee2e6;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #6c757d;
|
|
font-size: 14px;
|
|
text-align: center;
|
|
background: #f8f9fa;
|
|
touch-action: pan-y;
|
|
}
|
|
|
|
.test-results {
|
|
background: #e3f2fd;
|
|
border: 1px solid #2196f3;
|
|
border-radius: 6px;
|
|
padding: 15px;
|
|
margin-top: 15px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.test-results h4 {
|
|
margin-top: 0;
|
|
color: #1976d2;
|
|
}
|
|
|
|
.test-log {
|
|
background: #f5f5f5;
|
|
border-radius: 4px;
|
|
padding: 10px;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
max-height: 150px;
|
|
overflow-y: auto;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.device-info {
|
|
background: #fff3cd;
|
|
border: 1px solid #ffc107;
|
|
border-radius: 6px;
|
|
padding: 15px;
|
|
margin-bottom: 20px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.device-info h4 {
|
|
margin-top: 0;
|
|
color: #856404;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<h1>Mobile Touch Interactions Test</h1>
|
|
<p>Test the mobile touch features of the Bookmark Manager</p>
|
|
</header>
|
|
|
|
<div class="test-container">
|
|
<div class="device-info">
|
|
<h4>Device Information</h4>
|
|
<div id="deviceInfo">Loading device information...</div>
|
|
</div>
|
|
|
|
<div class="test-section">
|
|
<h3>1. Swipe Gestures Test</h3>
|
|
<div class="test-instructions">
|
|
<strong>Instructions:</strong><br>
|
|
• Swipe RIGHT on a bookmark to test the link<br>
|
|
• Swipe LEFT on a bookmark to delete it<br>
|
|
• Tap normally to show context menu<br>
|
|
• Minimum swipe distance: 100px
|
|
</div>
|
|
|
|
<div class="test-bookmark" data-bookmark-id="test1">
|
|
<div class="test-bookmark-info">
|
|
<div class="test-bookmark-title">Test Bookmark 1</div>
|
|
<div class="test-bookmark-url">https://example.com</div>
|
|
</div>
|
|
<div class="test-status">?</div>
|
|
</div>
|
|
|
|
<div class="test-bookmark" data-bookmark-id="test2">
|
|
<div class="test-bookmark-info">
|
|
<div class="test-bookmark-title">Test Bookmark 2</div>
|
|
<div class="test-bookmark-url">https://google.com</div>
|
|
</div>
|
|
<div class="test-status">?</div>
|
|
</div>
|
|
|
|
<div class="test-results">
|
|
<h4>Swipe Test Results</h4>
|
|
<div id="swipeResults">No swipe actions detected yet.</div>
|
|
<div class="test-log" id="swipeLog"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="test-section">
|
|
<h3>2. Pull-to-Refresh Test</h3>
|
|
<div class="test-instructions">
|
|
<strong>Instructions:</strong><br>
|
|
• Pull down from the top of the page<br>
|
|
• Pull at least 80px to trigger refresh<br>
|
|
• Watch for the refresh indicator
|
|
</div>
|
|
|
|
<div class="pull-refresh-area" id="pullRefreshArea">
|
|
Pull down from the top of the page to test pull-to-refresh
|
|
</div>
|
|
|
|
<div class="test-results">
|
|
<h4>Pull-to-Refresh Results</h4>
|
|
<div id="pullResults">No pull-to-refresh actions detected yet.</div>
|
|
<div class="test-log" id="pullLog"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="test-section">
|
|
<h3>3. Touch Target Size Test</h3>
|
|
<div class="test-instructions">
|
|
<strong>Instructions:</strong><br>
|
|
• All interactive elements should be at least 44px in size<br>
|
|
• Buttons should be easy to tap on mobile devices
|
|
</div>
|
|
|
|
<div style="display: flex; flex-wrap: wrap; gap: 10px; margin: 15px 0;">
|
|
<button class="btn btn-primary">Primary Button</button>
|
|
<button class="btn btn-secondary">Secondary</button>
|
|
<button class="btn btn-small">Small Button</button>
|
|
</div>
|
|
|
|
<div class="test-results">
|
|
<h4>Touch Target Analysis</h4>
|
|
<div id="touchTargetResults">Analyzing touch targets...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
class MobileTouchTester {
|
|
constructor() {
|
|
this.touchState = {
|
|
startX: 0,
|
|
startY: 0,
|
|
currentX: 0,
|
|
currentY: 0,
|
|
isDragging: false,
|
|
swipeThreshold: 100,
|
|
currentBookmark: null,
|
|
swipeDirection: null
|
|
};
|
|
|
|
this.pullToRefresh = {
|
|
startY: 0,
|
|
currentY: 0,
|
|
threshold: 80,
|
|
isActive: false,
|
|
isPulling: false
|
|
};
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.displayDeviceInfo();
|
|
this.initializeSwipeTest();
|
|
this.initializePullToRefreshTest();
|
|
this.analyzeTouchTargets();
|
|
}
|
|
|
|
displayDeviceInfo() {
|
|
const info = document.getElementById('deviceInfo');
|
|
const isMobile = this.isMobileDevice();
|
|
const hasTouch = 'ontouchstart' in window;
|
|
const maxTouchPoints = navigator.maxTouchPoints || 0;
|
|
|
|
info.innerHTML = `
|
|
<strong>User Agent:</strong> ${navigator.userAgent}<br>
|
|
<strong>Is Mobile Device:</strong> ${isMobile ? 'Yes' : 'No'}<br>
|
|
<strong>Touch Support:</strong> ${hasTouch ? 'Yes' : 'No'}<br>
|
|
<strong>Max Touch Points:</strong> ${maxTouchPoints}<br>
|
|
<strong>Screen Size:</strong> ${window.screen.width}x${window.screen.height}<br>
|
|
<strong>Viewport Size:</strong> ${window.innerWidth}x${window.innerHeight}
|
|
`;
|
|
}
|
|
|
|
isMobileDevice() {
|
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
|
('ontouchstart' in window) ||
|
|
(navigator.maxTouchPoints > 0);
|
|
}
|
|
|
|
initializeSwipeTest() {
|
|
const bookmarks = document.querySelectorAll('.test-bookmark');
|
|
|
|
bookmarks.forEach(bookmark => {
|
|
bookmark.addEventListener('touchstart', this.handleTouchStart.bind(this), { passive: false });
|
|
bookmark.addEventListener('touchmove', this.handleTouchMove.bind(this), { passive: false });
|
|
bookmark.addEventListener('touchend', this.handleTouchEnd.bind(this), { passive: false });
|
|
});
|
|
}
|
|
|
|
handleTouchStart(e) {
|
|
const touch = e.touches[0];
|
|
const bookmarkItem = e.currentTarget;
|
|
|
|
this.touchState.startX = touch.clientX;
|
|
this.touchState.startY = touch.clientY;
|
|
this.touchState.currentX = touch.clientX;
|
|
this.touchState.currentY = touch.clientY;
|
|
this.touchState.isDragging = false;
|
|
this.touchState.currentBookmark = bookmarkItem;
|
|
this.touchState.swipeDirection = null;
|
|
|
|
bookmarkItem.classList.add('swiping');
|
|
this.logSwipeAction(`Touch start at (${touch.clientX}, ${touch.clientY})`);
|
|
}
|
|
|
|
handleTouchMove(e) {
|
|
if (!this.touchState.currentBookmark) return;
|
|
|
|
const touch = e.touches[0];
|
|
const bookmarkItem = e.currentTarget;
|
|
|
|
this.touchState.currentX = touch.clientX;
|
|
this.touchState.currentY = touch.clientY;
|
|
|
|
const deltaX = this.touchState.currentX - this.touchState.startX;
|
|
const deltaY = this.touchState.currentY - this.touchState.startY;
|
|
|
|
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 10) {
|
|
e.preventDefault();
|
|
this.touchState.isDragging = true;
|
|
|
|
bookmarkItem.style.setProperty('--swipe-offset', `${deltaX}px`);
|
|
|
|
if (deltaX > 30) {
|
|
bookmarkItem.classList.add('swipe-right');
|
|
bookmarkItem.classList.remove('swipe-left');
|
|
this.touchState.swipeDirection = 'right';
|
|
} else if (deltaX < -30) {
|
|
bookmarkItem.classList.add('swipe-left');
|
|
bookmarkItem.classList.remove('swipe-right');
|
|
this.touchState.swipeDirection = 'left';
|
|
} else {
|
|
bookmarkItem.classList.remove('swipe-left', 'swipe-right');
|
|
this.touchState.swipeDirection = null;
|
|
}
|
|
|
|
this.logSwipeAction(`Swiping ${this.touchState.swipeDirection || 'neutral'}: ${deltaX}px`);
|
|
}
|
|
}
|
|
|
|
handleTouchEnd(e) {
|
|
const bookmarkItem = e.currentTarget;
|
|
const deltaX = this.touchState.currentX - this.touchState.startX;
|
|
|
|
bookmarkItem.classList.remove('swiping', 'swipe-left', 'swipe-right');
|
|
bookmarkItem.style.removeProperty('--swipe-offset');
|
|
|
|
if (this.touchState.isDragging && Math.abs(deltaX) > this.touchState.swipeThreshold) {
|
|
if (this.touchState.swipeDirection === 'right') {
|
|
this.handleSwipeRight(bookmarkItem);
|
|
} else if (this.touchState.swipeDirection === 'left') {
|
|
this.handleSwipeLeft(bookmarkItem);
|
|
}
|
|
} else if (!this.touchState.isDragging) {
|
|
this.handleTap(bookmarkItem);
|
|
}
|
|
|
|
this.resetTouchState();
|
|
}
|
|
|
|
handleSwipeRight(bookmark) {
|
|
const title = bookmark.querySelector('.test-bookmark-title').textContent;
|
|
this.logSwipeAction(`✅ Swipe RIGHT detected on "${title}" - Testing link`);
|
|
this.showFeedback('Testing link...', 'success');
|
|
}
|
|
|
|
handleSwipeLeft(bookmark) {
|
|
const title = bookmark.querySelector('.test-bookmark-title').textContent;
|
|
this.logSwipeAction(`❌ Swipe LEFT detected on "${title}" - Deleting bookmark`);
|
|
this.showFeedback('Bookmark deleted', 'danger');
|
|
}
|
|
|
|
handleTap(bookmark) {
|
|
const title = bookmark.querySelector('.test-bookmark-title').textContent;
|
|
this.logSwipeAction(`👆 TAP detected on "${title}" - Showing context menu`);
|
|
this.showFeedback('Context menu opened', 'info');
|
|
}
|
|
|
|
resetTouchState() {
|
|
this.touchState = {
|
|
startX: 0,
|
|
startY: 0,
|
|
currentX: 0,
|
|
currentY: 0,
|
|
isDragging: false,
|
|
swipeThreshold: 100,
|
|
currentBookmark: null,
|
|
swipeDirection: null
|
|
};
|
|
}
|
|
|
|
initializePullToRefreshTest() {
|
|
document.addEventListener('touchstart', this.handlePullStart.bind(this), { passive: false });
|
|
document.addEventListener('touchmove', this.handlePullMove.bind(this), { passive: false });
|
|
document.addEventListener('touchend', this.handlePullEnd.bind(this), { passive: false });
|
|
}
|
|
|
|
handlePullStart(e) {
|
|
if (window.scrollY > 0) return;
|
|
|
|
const touch = e.touches[0];
|
|
this.pullToRefresh.startY = touch.clientY;
|
|
this.pullToRefresh.currentY = touch.clientY;
|
|
this.pullToRefresh.isPulling = false;
|
|
|
|
this.logPullAction(`Pull start at Y: ${touch.clientY}`);
|
|
}
|
|
|
|
handlePullMove(e) {
|
|
if (window.scrollY > 0 || !this.pullToRefresh.startY) return;
|
|
|
|
const touch = e.touches[0];
|
|
this.pullToRefresh.currentY = touch.clientY;
|
|
const deltaY = this.pullToRefresh.currentY - this.pullToRefresh.startY;
|
|
|
|
if (deltaY > 0) {
|
|
e.preventDefault();
|
|
this.pullToRefresh.isPulling = true;
|
|
|
|
const progress = Math.min(deltaY / this.pullToRefresh.threshold, 1);
|
|
const area = document.getElementById('pullRefreshArea');
|
|
|
|
if (deltaY > this.pullToRefresh.threshold) {
|
|
area.style.background = '#d4edda';
|
|
area.style.borderColor = '#28a745';
|
|
area.innerHTML = `🔄 Release to refresh (${Math.round(deltaY)}px)`;
|
|
this.logPullAction(`Pull threshold exceeded: ${deltaY}px`);
|
|
} else {
|
|
area.style.background = '#fff3cd';
|
|
area.style.borderColor = '#ffc107';
|
|
area.innerHTML = `⬇️ Pull to refresh (${Math.round(deltaY)}px / ${this.pullToRefresh.threshold}px)`;
|
|
}
|
|
}
|
|
}
|
|
|
|
handlePullEnd(e) {
|
|
if (!this.pullToRefresh.isPulling) return;
|
|
|
|
const deltaY = this.pullToRefresh.currentY - this.pullToRefresh.startY;
|
|
const area = document.getElementById('pullRefreshArea');
|
|
|
|
area.style.background = '#f8f9fa';
|
|
area.style.borderColor = '#dee2e6';
|
|
area.innerHTML = 'Pull down from the top of the page to test pull-to-refresh';
|
|
|
|
if (deltaY > this.pullToRefresh.threshold) {
|
|
this.triggerPullToRefresh();
|
|
}
|
|
|
|
this.pullToRefresh.startY = 0;
|
|
this.pullToRefresh.currentY = 0;
|
|
this.pullToRefresh.isPulling = false;
|
|
}
|
|
|
|
triggerPullToRefresh() {
|
|
this.logPullAction('✅ Pull-to-refresh triggered!');
|
|
this.showFeedback('Refreshing...', 'info');
|
|
}
|
|
|
|
analyzeTouchTargets() {
|
|
const buttons = document.querySelectorAll('.btn');
|
|
const results = document.getElementById('touchTargetResults');
|
|
let analysis = '';
|
|
|
|
buttons.forEach((button, index) => {
|
|
const rect = button.getBoundingClientRect();
|
|
const width = rect.width;
|
|
const height = rect.height;
|
|
const meetsStandard = width >= 44 && height >= 44;
|
|
|
|
analysis += `Button ${index + 1}: ${width.toFixed(0)}x${height.toFixed(0)}px ${meetsStandard ? '✅' : '❌'}<br>`;
|
|
});
|
|
|
|
results.innerHTML = analysis;
|
|
}
|
|
|
|
showFeedback(message, type = 'info') {
|
|
let feedback = document.getElementById('mobile-feedback');
|
|
if (!feedback) {
|
|
feedback = document.createElement('div');
|
|
feedback.id = 'mobile-feedback';
|
|
feedback.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
padding: 12px 24px;
|
|
border-radius: 8px;
|
|
color: white;
|
|
font-weight: 500;
|
|
z-index: 1002;
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
pointer-events: none;
|
|
`;
|
|
document.body.appendChild(feedback);
|
|
}
|
|
|
|
const colors = {
|
|
success: '#28a745',
|
|
danger: '#dc3545',
|
|
info: '#17a2b8',
|
|
warning: '#ffc107'
|
|
};
|
|
|
|
feedback.textContent = message;
|
|
feedback.style.backgroundColor = colors[type] || colors.info;
|
|
feedback.style.opacity = '1';
|
|
|
|
setTimeout(() => {
|
|
feedback.style.opacity = '0';
|
|
}, 2000);
|
|
}
|
|
|
|
logSwipeAction(message) {
|
|
const log = document.getElementById('swipeLog');
|
|
const results = document.getElementById('swipeResults');
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
|
|
log.innerHTML += `[${timestamp}] ${message}\n`;
|
|
log.scrollTop = log.scrollHeight;
|
|
|
|
results.textContent = `Last action: ${message}`;
|
|
}
|
|
|
|
logPullAction(message) {
|
|
const log = document.getElementById('pullLog');
|
|
const results = document.getElementById('pullResults');
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
|
|
log.innerHTML += `[${timestamp}] ${message}\n`;
|
|
log.scrollTop = log.scrollHeight;
|
|
|
|
results.textContent = `Last action: ${message}`;
|
|
}
|
|
}
|
|
|
|
// Initialize the tester when the page loads
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
new MobileTouchTester();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |