feat: major project restructure - move Django to backend dir and fix critical imports

- Restructure project: moved Django backend to backend/ directory
- Add frontend/ directory for future Next.js application
- Add shared/ directory for common resources
- Fix critical Django import errors:
  - Add missing sys.path modification for apps directory
  - Fix undefined CATEGORY_CHOICES imports in rides module
  - Fix media migration undefined references
  - Remove unused imports and f-strings without placeholders
- Install missing django-environ dependency
- Django server now runs without ModuleNotFoundError
- Update .gitignore and README for new structure
- Add pnpm workspace configuration for monorepo setup
This commit is contained in:
pacnpal
2025-08-23 18:37:55 -04:00
parent 652ea149bd
commit b0e0678590
996 changed files with 370 additions and 192768 deletions

View File

@@ -1,881 +0,0 @@
/**
* ThrillWiki Mobile Touch Support - Enhanced Mobile and Touch Experience
*
* This module provides mobile-optimized interactions, touch-friendly controls,
* responsive map sizing, and battery-conscious features for mobile devices
*/
class MobileTouchSupport {
constructor(options = {}) {
this.options = {
enableTouchOptimizations: true,
enableSwipeGestures: true,
enablePinchZoom: true,
enableResponsiveResize: true,
enableBatteryOptimization: true,
touchDebounceDelay: 150,
swipeThreshold: 50,
swipeVelocityThreshold: 0.3,
maxTouchPoints: 2,
orientationChangeDelay: 300,
...options
};
this.isMobile = this.detectMobileDevice();
this.isTouch = this.detectTouchSupport();
this.orientation = this.getOrientation();
this.mapInstances = new Set();
this.touchHandlers = new Map();
this.gestureState = {
isActive: false,
startDistance: 0,
startCenter: null,
lastTouchTime: 0
};
this.init();
}
/**
* Initialize mobile touch support
*/
init() {
if (!this.isTouch && !this.isMobile) {
console.log('Mobile touch support not needed for this device');
return;
}
this.setupTouchOptimizations();
this.setupSwipeGestures();
this.setupResponsiveHandling();
this.setupBatteryOptimization();
this.setupAccessibilityEnhancements();
this.bindEventHandlers();
console.log('Mobile touch support initialized');
}
/**
* Detect if device is mobile
*/
detectMobileDevice() {
const userAgent = navigator.userAgent.toLowerCase();
const mobileKeywords = ['android', 'iphone', 'ipad', 'ipod', 'blackberry', 'windows phone'];
return mobileKeywords.some(keyword => userAgent.includes(keyword)) ||
window.innerWidth <= 768 ||
(typeof window.orientation !== 'undefined');
}
/**
* Detect touch support
*/
detectTouchSupport() {
return 'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0;
}
/**
* Get current orientation
*/
getOrientation() {
if (screen.orientation) {
return screen.orientation.angle;
} else if (window.orientation !== undefined) {
return window.orientation;
}
return window.innerWidth > window.innerHeight ? 90 : 0;
}
/**
* Setup touch optimizations
*/
setupTouchOptimizations() {
if (!this.options.enableTouchOptimizations) return;
// Add touch-optimized styles
this.addTouchStyles();
// Enhance touch targets
this.enhanceTouchTargets();
// Optimize scroll behavior
this.optimizeScrollBehavior();
// Setup touch feedback
this.setupTouchFeedback();
}
/**
* Add touch-optimized CSS styles
*/
addTouchStyles() {
if (document.getElementById('mobile-touch-styles')) return;
const styles = `
<style id="mobile-touch-styles">
@media (max-width: 768px) {
/* Touch-friendly button sizes */
.btn, button, .filter-chip, .filter-pill {
min-height: 44px;
min-width: 44px;
padding: 12px 16px;
font-size: 16px;
}
/* Larger touch targets for map controls */
.leaflet-control-zoom a {
width: 44px !important;
height: 44px !important;
line-height: 44px !important;
font-size: 18px !important;
}
/* Mobile-optimized map containers */
.map-container {
height: 60vh !important;
min-height: 300px !important;
}
/* Touch-friendly popup styling */
.leaflet-popup-content-wrapper {
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}
.leaflet-popup-content {
margin: 16px 20px;
font-size: 16px;
line-height: 1.5;
}
/* Improved form controls */
input, select, textarea {
font-size: 16px !important; /* Prevents zoom on iOS */
padding: 12px;
border-radius: 8px;
}
/* Touch-friendly filter panels */
.filter-panel {
padding: 16px;
border-radius: 12px;
}
/* Mobile navigation improvements */
.roadtrip-planner {
padding: 16px;
}
.parks-list .park-item {
padding: 16px;
margin-bottom: 12px;
border-radius: 12px;
touch-action: manipulation;
}
/* Swipe indicators */
.swipe-indicator {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 40px;
background: rgba(59, 130, 246, 0.5);
border-radius: 2px;
transition: opacity 0.3s ease;
}
.swipe-indicator.left {
left: 8px;
}
.swipe-indicator.right {
right: 8px;
}
}
@media (max-width: 480px) {
/* Extra small screens */
.map-container {
height: 50vh !important;
}
.filter-panel {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
max-height: 50vh;
overflow-y: auto;
border-radius: 16px 16px 0 0;
box-shadow: 0 -4px 20px rgba(0,0,0,0.15);
}
}
/* Touch feedback */
.touch-feedback {
position: relative;
overflow: hidden;
}
.touch-feedback::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.3s ease, height 0.3s ease;
pointer-events: none;
}
.touch-feedback.active::after {
width: 100px;
height: 100px;
}
/* Prevent text selection on mobile */
.no-select {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* Optimize touch scrolling */
.touch-scroll {
-webkit-overflow-scrolling: touch;
overflow-scrolling: touch;
}
</style>
`;
document.head.insertAdjacentHTML('beforeend', styles);
}
/**
* Enhance touch targets for better accessibility
*/
enhanceTouchTargets() {
const smallTargets = document.querySelectorAll('button, .btn, a, input[type="checkbox"], input[type="radio"]');
smallTargets.forEach(target => {
const rect = target.getBoundingClientRect();
// If target is smaller than 44px (Apple's recommended minimum), enhance it
if (rect.width < 44 || rect.height < 44) {
target.classList.add('touch-enhanced');
target.style.minWidth = '44px';
target.style.minHeight = '44px';
target.style.display = 'inline-flex';
target.style.alignItems = 'center';
target.style.justifyContent = 'center';
}
});
}
/**
* Optimize scroll behavior for mobile
*/
optimizeScrollBehavior() {
// Add momentum scrolling to scrollable elements
const scrollableElements = document.querySelectorAll('.scrollable, .overflow-auto, .overflow-y-auto');
scrollableElements.forEach(element => {
element.classList.add('touch-scroll');
element.style.webkitOverflowScrolling = 'touch';
});
// Prevent body scroll when interacting with maps
document.addEventListener('touchstart', (e) => {
if (e.target.closest('.leaflet-container')) {
e.preventDefault();
}
}, { passive: false });
}
/**
* Setup touch feedback for interactive elements
*/
setupTouchFeedback() {
const interactiveElements = document.querySelectorAll('button, .btn, .filter-chip, .filter-pill, .park-item');
interactiveElements.forEach(element => {
element.classList.add('touch-feedback');
element.addEventListener('touchstart', (e) => {
element.classList.add('active');
setTimeout(() => {
element.classList.remove('active');
}, 300);
}, { passive: true });
});
}
/**
* Setup swipe gesture support
*/
setupSwipeGestures() {
if (!this.options.enableSwipeGestures) return;
let touchStartX = 0;
let touchStartY = 0;
let touchStartTime = 0;
document.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
touchStartTime = Date.now();
}
}, { passive: true });
document.addEventListener('touchend', (e) => {
if (e.changedTouches.length === 1) {
const touchEndX = e.changedTouches[0].clientX;
const touchEndY = e.changedTouches[0].clientY;
const touchEndTime = Date.now();
const deltaX = touchEndX - touchStartX;
const deltaY = touchEndY - touchStartY;
const deltaTime = touchEndTime - touchStartTime;
const velocity = Math.abs(deltaX) / deltaTime;
// Check if this is a swipe gesture
if (Math.abs(deltaX) > this.options.swipeThreshold &&
Math.abs(deltaY) < Math.abs(deltaX) &&
velocity > this.options.swipeVelocityThreshold) {
const direction = deltaX > 0 ? 'right' : 'left';
this.handleSwipeGesture(direction, e.target);
}
}
}, { passive: true });
}
/**
* Handle swipe gestures
*/
handleSwipeGesture(direction, target) {
// Handle swipe on filter panels
if (target.closest('.filter-panel')) {
if (direction === 'down' || direction === 'up') {
this.toggleFilterPanel();
}
}
// Handle swipe on road trip list
if (target.closest('.parks-list')) {
if (direction === 'left') {
this.showParkActions(target);
} else if (direction === 'right') {
this.hideParkActions(target);
}
}
// Emit custom swipe event
const swipeEvent = new CustomEvent('swipe', {
detail: { direction, target }
});
document.dispatchEvent(swipeEvent);
}
/**
* Setup responsive handling for orientation changes
*/
setupResponsiveHandling() {
if (!this.options.enableResponsiveResize) return;
// Handle orientation changes
window.addEventListener('orientationchange', () => {
setTimeout(() => {
this.handleOrientationChange();
}, this.options.orientationChangeDelay);
});
// Handle window resize
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
this.handleWindowResize();
}, 250);
});
// Handle viewport changes (for mobile browsers with dynamic toolbars)
this.setupViewportHandler();
}
/**
* Handle orientation change
*/
handleOrientationChange() {
const newOrientation = this.getOrientation();
if (newOrientation !== this.orientation) {
this.orientation = newOrientation;
// Resize all map instances
this.mapInstances.forEach(mapInstance => {
if (mapInstance.invalidateSize) {
mapInstance.invalidateSize();
}
});
// Emit orientation change event
const orientationEvent = new CustomEvent('orientationChanged', {
detail: { orientation: this.orientation }
});
document.dispatchEvent(orientationEvent);
}
}
/**
* Handle window resize
*/
handleWindowResize() {
// Update mobile detection
this.isMobile = this.detectMobileDevice();
// Resize map instances
this.mapInstances.forEach(mapInstance => {
if (mapInstance.invalidateSize) {
mapInstance.invalidateSize();
}
});
// Update touch targets
this.enhanceTouchTargets();
}
/**
* Setup viewport handler for dynamic mobile toolbars
*/
setupViewportHandler() {
// Use visual viewport API if available
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', () => {
this.handleViewportChange();
});
}
// Fallback for older browsers
let lastHeight = window.innerHeight;
const checkViewportChange = () => {
if (Math.abs(window.innerHeight - lastHeight) > 100) {
lastHeight = window.innerHeight;
this.handleViewportChange();
}
};
window.addEventListener('resize', checkViewportChange);
document.addEventListener('focusin', checkViewportChange);
document.addEventListener('focusout', checkViewportChange);
}
/**
* Handle viewport changes
*/
handleViewportChange() {
// Adjust map container heights
const mapContainers = document.querySelectorAll('.map-container');
mapContainers.forEach(container => {
const viewportHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight;
if (viewportHeight < 500) {
container.style.height = '40vh';
} else {
container.style.height = ''; // Reset to CSS default
}
});
}
/**
* Setup battery optimization
*/
setupBatteryOptimization() {
if (!this.options.enableBatteryOptimization) return;
// Reduce update frequency when battery is low
if ('getBattery' in navigator) {
navigator.getBattery().then(battery => {
const optimizeBattery = () => {
if (battery.level < 0.2) { // Battery below 20%
this.enableBatterySaveMode();
} else {
this.disableBatterySaveMode();
}
};
battery.addEventListener('levelchange', optimizeBattery);
optimizeBattery();
});
}
// Reduce activity when page is not visible
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.pauseNonEssentialFeatures();
} else {
this.resumeNonEssentialFeatures();
}
});
}
/**
* Enable battery save mode
*/
enableBatterySaveMode() {
console.log('Enabling battery save mode');
// Reduce map update frequency
this.mapInstances.forEach(mapInstance => {
if (mapInstance.options) {
mapInstance.options.updateInterval = 5000; // Increase to 5 seconds
}
});
// Disable animations
document.body.classList.add('battery-save-mode');
}
/**
* Disable battery save mode
*/
disableBatterySaveMode() {
console.log('Disabling battery save mode');
// Restore normal update frequency
this.mapInstances.forEach(mapInstance => {
if (mapInstance.options) {
mapInstance.options.updateInterval = 1000; // Restore to 1 second
}
});
// Re-enable animations
document.body.classList.remove('battery-save-mode');
}
/**
* Pause non-essential features
*/
pauseNonEssentialFeatures() {
// Pause location watching
if (window.userLocation && window.userLocation.stopWatching) {
window.userLocation.stopWatching();
}
// Reduce map updates
this.mapInstances.forEach(mapInstance => {
if (mapInstance.pauseUpdates) {
mapInstance.pauseUpdates();
}
});
}
/**
* Resume non-essential features
*/
resumeNonEssentialFeatures() {
// Resume location watching if it was active
if (window.userLocation && window.userLocation.options.watchPosition) {
window.userLocation.startWatching();
}
// Resume map updates
this.mapInstances.forEach(mapInstance => {
if (mapInstance.resumeUpdates) {
mapInstance.resumeUpdates();
}
});
}
/**
* Setup accessibility enhancements for mobile
*/
setupAccessibilityEnhancements() {
// Add focus indicators for touch navigation
const focusableElements = document.querySelectorAll('button, a, input, select, textarea, [tabindex]');
focusableElements.forEach(element => {
element.addEventListener('focus', () => {
element.classList.add('touch-focused');
});
element.addEventListener('blur', () => {
element.classList.remove('touch-focused');
});
});
// Enhance keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
document.body.classList.add('keyboard-navigation');
}
});
document.addEventListener('mousedown', () => {
document.body.classList.remove('keyboard-navigation');
});
}
/**
* Bind event handlers
*/
bindEventHandlers() {
// Handle double-tap to zoom
this.setupDoubleTapZoom();
// Handle long press
this.setupLongPress();
// Handle pinch gestures
if (this.options.enablePinchZoom) {
this.setupPinchZoom();
}
}
/**
* Setup double-tap to zoom
*/
setupDoubleTapZoom() {
let lastTapTime = 0;
document.addEventListener('touchend', (e) => {
const currentTime = Date.now();
if (currentTime - lastTapTime < 300) {
// Double tap detected
const target = e.target;
if (target.closest('.leaflet-container')) {
this.handleDoubleTapZoom(e);
}
}
lastTapTime = currentTime;
}, { passive: true });
}
/**
* Handle double-tap zoom
*/
handleDoubleTapZoom(e) {
const mapContainer = e.target.closest('.leaflet-container');
if (!mapContainer) return;
// Find associated map instance
this.mapInstances.forEach(mapInstance => {
if (mapInstance.getContainer() === mapContainer) {
const currentZoom = mapInstance.getZoom();
const newZoom = currentZoom < mapInstance.getMaxZoom() ? currentZoom + 2 : mapInstance.getMinZoom();
mapInstance.setZoom(newZoom, {
animate: true,
duration: 0.3
});
}
});
}
/**
* Setup long press detection
*/
setupLongPress() {
let pressTimer;
document.addEventListener('touchstart', (e) => {
pressTimer = setTimeout(() => {
this.handleLongPress(e);
}, 750); // 750ms for long press
}, { passive: true });
document.addEventListener('touchend', () => {
clearTimeout(pressTimer);
}, { passive: true });
document.addEventListener('touchmove', () => {
clearTimeout(pressTimer);
}, { passive: true });
}
/**
* Handle long press
*/
handleLongPress(e) {
const target = e.target;
// Emit long press event
const longPressEvent = new CustomEvent('longPress', {
detail: { target, touches: e.touches }
});
target.dispatchEvent(longPressEvent);
// Provide haptic feedback if available
if (navigator.vibrate) {
navigator.vibrate(50);
}
}
/**
* Setup pinch zoom for maps
*/
setupPinchZoom() {
document.addEventListener('touchstart', (e) => {
if (e.touches.length === 2) {
this.gestureState.isActive = true;
this.gestureState.startDistance = this.getDistance(e.touches[0], e.touches[1]);
this.gestureState.startCenter = this.getCenter(e.touches[0], e.touches[1]);
}
}, { passive: true });
document.addEventListener('touchmove', (e) => {
if (this.gestureState.isActive && e.touches.length === 2) {
this.handlePinchZoom(e);
}
}, { passive: false });
document.addEventListener('touchend', () => {
this.gestureState.isActive = false;
}, { passive: true });
}
/**
* Handle pinch zoom gesture
*/
handlePinchZoom(e) {
if (!e.target.closest('.leaflet-container')) return;
const currentDistance = this.getDistance(e.touches[0], e.touches[1]);
const scale = currentDistance / this.gestureState.startDistance;
// Emit pinch event
const pinchEvent = new CustomEvent('pinch', {
detail: { scale, center: this.gestureState.startCenter }
});
e.target.dispatchEvent(pinchEvent);
}
/**
* Get distance between two touch points
*/
getDistance(touch1, touch2) {
const dx = touch1.clientX - touch2.clientX;
const dy = touch1.clientY - touch2.clientY;
return Math.sqrt(dx * dx + dy * dy);
}
/**
* Get center point between two touches
*/
getCenter(touch1, touch2) {
return {
x: (touch1.clientX + touch2.clientX) / 2,
y: (touch1.clientY + touch2.clientY) / 2
};
}
/**
* Register map instance for mobile optimizations
*/
registerMapInstance(mapInstance) {
this.mapInstances.add(mapInstance);
// Apply mobile-specific map options
if (this.isMobile && mapInstance.options) {
mapInstance.options.zoomControl = false; // Use custom larger controls
mapInstance.options.attributionControl = false; // Save space
}
}
/**
* Unregister map instance
*/
unregisterMapInstance(mapInstance) {
this.mapInstances.delete(mapInstance);
}
/**
* Toggle filter panel for mobile
*/
toggleFilterPanel() {
const filterPanel = document.querySelector('.filter-panel');
if (filterPanel) {
filterPanel.classList.toggle('mobile-expanded');
}
}
/**
* Show park actions on swipe
*/
showParkActions(target) {
const parkItem = target.closest('.park-item');
if (parkItem) {
parkItem.classList.add('actions-visible');
}
}
/**
* Hide park actions
*/
hideParkActions(target) {
const parkItem = target.closest('.park-item');
if (parkItem) {
parkItem.classList.remove('actions-visible');
}
}
/**
* Check if device is mobile
*/
isMobileDevice() {
return this.isMobile;
}
/**
* Check if device supports touch
*/
isTouchDevice() {
return this.isTouch;
}
/**
* Get device info
*/
getDeviceInfo() {
return {
isMobile: this.isMobile,
isTouch: this.isTouch,
orientation: this.orientation,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
pixelRatio: window.devicePixelRatio || 1
};
}
}
// Auto-initialize mobile touch support
document.addEventListener('DOMContentLoaded', function() {
window.mobileTouchSupport = new MobileTouchSupport();
// Register existing map instances
if (window.thrillwikiMap) {
window.mobileTouchSupport.registerMapInstance(window.thrillwikiMap);
}
});
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = MobileTouchSupport;
} else {
window.MobileTouchSupport = MobileTouchSupport;
}