Browse Source
- Change server switching logs from info to debug level - Implement structured error logging for profile CRUD operations - Handle HTTP status codes 400, 401, 403, 404, 409 gracefully - Suppress full error stack traces for expected API responses - Maintain user notifications while improving console readability - Add timestamp and context to all profile-related error logs Improves developer experience by reducing console noise while preserving debugging information and user-facing error handling.pull/170/head
59 changed files with 1244 additions and 675 deletions
@ -0,0 +1,44 @@ |
|||||
|
package app.timesafari.safearea; |
||||
|
|
||||
|
import android.os.Build; |
||||
|
import android.view.WindowInsets; |
||||
|
import com.getcapacitor.JSObject; |
||||
|
import com.getcapacitor.Plugin; |
||||
|
import com.getcapacitor.PluginCall; |
||||
|
import com.getcapacitor.PluginMethod; |
||||
|
import com.getcapacitor.annotation.CapacitorPlugin; |
||||
|
|
||||
|
@CapacitorPlugin(name = "SafeArea") |
||||
|
public class SafeAreaPlugin extends Plugin { |
||||
|
|
||||
|
@PluginMethod |
||||
|
public void getSafeAreaInsets(PluginCall call) { |
||||
|
JSObject result = new JSObject(); |
||||
|
|
||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { |
||||
|
WindowInsets insets = getActivity().getWindow().getDecorView().getRootWindowInsets(); |
||||
|
if (insets != null) { |
||||
|
int top = insets.getInsets(WindowInsets.Type.statusBars()).top; |
||||
|
int bottom = insets.getInsets(WindowInsets.Type.navigationBars()).bottom; |
||||
|
int left = insets.getInsets(WindowInsets.Type.systemBars()).left; |
||||
|
int right = insets.getInsets(WindowInsets.Type.systemBars()).right; |
||||
|
|
||||
|
result.put("top", top); |
||||
|
result.put("bottom", bottom); |
||||
|
result.put("left", left); |
||||
|
result.put("right", right); |
||||
|
|
||||
|
call.resolve(result); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Fallback values
|
||||
|
result.put("top", 0); |
||||
|
result.put("bottom", 0); |
||||
|
result.put("left", 0); |
||||
|
result.put("right", 0); |
||||
|
|
||||
|
call.resolve(result); |
||||
|
} |
||||
|
} |
@ -0,0 +1,69 @@ |
|||||
|
# Z-Index Guide — TimeSafari |
||||
|
|
||||
|
**Author**: Development Team |
||||
|
**Date**: 2025-08-25T19:38:09-08:00 |
||||
|
**Status**: 🎯 **ACTIVE** - Z-index layering standards |
||||
|
|
||||
|
## Objective |
||||
|
Establish consistent z-index values across the TimeSafari application to ensure proper layering of UI elements. |
||||
|
|
||||
|
## Result |
||||
|
This document defines the z-index hierarchy for all UI components. |
||||
|
|
||||
|
## Use/Run |
||||
|
Reference these values when implementing new components or modifying existing ones to maintain consistent layering. |
||||
|
|
||||
|
## Z-Index Hierarchy |
||||
|
|
||||
|
| Component | Z-Index | Usage | |
||||
|
|-----------|---------|-------| |
||||
|
| **Map** | `40` | Base map layer and map-related overlays | |
||||
|
| **QuickNav** | `50` | Quick navigation bottom bar | |
||||
|
| **Dialogs and Modals** | `100` | Modal dialogs, popups, and overlay content | |
||||
|
| **Notifications and Toasts** | `120` | System notifications, alerts, and toast messages | |
||||
|
|
||||
|
## Best Practices |
||||
|
|
||||
|
1. **Never exceed 120** - Keep the highest z-index reserved for critical notifications |
||||
|
2. **Use increments of 10** - Leave room for future additions between layers |
||||
|
3. **Document exceptions** - If you need a z-index outside this range, document the reason |
||||
|
4. **Test layering** - Verify z-index behavior across different screen sizes and devices |
||||
|
|
||||
|
## Common Pitfalls |
||||
|
|
||||
|
- **Avoid arbitrary values** - Don't use random z-index numbers |
||||
|
- **Don't nest high z-index** - Keep child elements within their parent's z-index range |
||||
|
- **Consider stacking context** - Remember that `position: relative` creates new stacking contexts |
||||
|
|
||||
|
## Next Steps |
||||
|
|
||||
|
| Owner | Task | Exit Criteria | Target Date | |
||||
|
|-------|------|---------------|-------------| |
||||
|
| Dev Team | Apply z-index classes to existing components | All components use defined z-index values | 2025-09-01 | |
||||
|
|
||||
|
## Competence Hooks |
||||
|
|
||||
|
- **Why this works**: Creates predictable layering hierarchy that prevents UI conflicts |
||||
|
- **Common pitfalls**: Using arbitrary z-index values or exceeding the defined range |
||||
|
- **Next skill unlock**: Learn about CSS stacking contexts and their impact on z-index |
||||
|
- **Teach-back**: Explain the z-index hierarchy to a team member without referencing this guide |
||||
|
|
||||
|
## Collaboration Hooks |
||||
|
|
||||
|
- **Reviewers**: Frontend team, UI/UX designers |
||||
|
- **Sign-off checklist**: |
||||
|
- [ ] All new components follow z-index guidelines |
||||
|
- [ ] Existing components updated to use defined values |
||||
|
- [ ] Cross-browser testing completed |
||||
|
- [ ] Mobile responsiveness verified |
||||
|
|
||||
|
## Assumptions & Limits |
||||
|
|
||||
|
- Assumes modern browser support for z-index |
||||
|
- Limited to 4 defined layers (expandable if needed) |
||||
|
- Requires team discipline to maintain consistency |
||||
|
|
||||
|
## References |
||||
|
|
||||
|
- [MDN Z-Index Documentation](https://developer.mozilla.org/en-US/docs/Web/CSS/z-index) |
||||
|
- [CSS Stacking Context Guide](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context) |
@ -0,0 +1,14 @@ |
|||||
|
/** |
||||
|
* Constants for entity-related strings, particularly for unnamed/unknown person entities |
||||
|
*/ |
||||
|
|
||||
|
// Core unnamed entity names
|
||||
|
export const UNNAMED_ENTITY_NAME = "Unnamed"; |
||||
|
|
||||
|
// Descriptive phrases for unnamed entities
|
||||
|
export const SOMEONE_UNNAMED = "Someone Unnamed"; |
||||
|
export const THAT_UNNAMED_PERSON = "That unnamed person"; |
||||
|
export const UNNAMED_PERSON = "unnamed person"; |
||||
|
|
||||
|
// Project-related unnamed entities
|
||||
|
export const UNNAMED_PROJECT = "Unnamed Project"; |
@ -0,0 +1,185 @@ |
|||||
|
import { Capacitor } from "@capacitor/core"; |
||||
|
import { Clipboard } from "@capacitor/clipboard"; |
||||
|
import { useClipboard } from "@vueuse/core"; |
||||
|
import { logger } from "@/utils/logger"; |
||||
|
|
||||
|
/** |
||||
|
* Platform-agnostic clipboard service that handles both web and native platforms |
||||
|
* Provides reliable clipboard functionality across all platforms including iOS |
||||
|
*/ |
||||
|
export class ClipboardService { |
||||
|
private static instance: ClipboardService | null = null; |
||||
|
|
||||
|
/** |
||||
|
* Get singleton instance of ClipboardService |
||||
|
*/ |
||||
|
public static getInstance(): ClipboardService { |
||||
|
if (!ClipboardService.instance) { |
||||
|
ClipboardService.instance = new ClipboardService(); |
||||
|
} |
||||
|
return ClipboardService.instance; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Copy text to clipboard with platform-specific handling |
||||
|
* |
||||
|
* @param text - The text to copy to clipboard |
||||
|
* @returns Promise that resolves when copy is complete |
||||
|
* @throws Error if copy operation fails |
||||
|
*/ |
||||
|
public async copyToClipboard(text: string): Promise<void> { |
||||
|
const platform = Capacitor.getPlatform(); |
||||
|
const isNative = Capacitor.isNativePlatform(); |
||||
|
|
||||
|
logger.debug("[ClipboardService] Copying to clipboard:", { |
||||
|
text: text.substring(0, 50) + (text.length > 50 ? "..." : ""), |
||||
|
platform, |
||||
|
isNative, |
||||
|
timestamp: new Date().toISOString(), |
||||
|
}); |
||||
|
|
||||
|
try { |
||||
|
if (isNative && (platform === "ios" || platform === "android")) { |
||||
|
// Use native Capacitor clipboard for mobile platforms
|
||||
|
await this.copyNative(text); |
||||
|
} else { |
||||
|
// Use web clipboard API for web/desktop platforms
|
||||
|
await this.copyWeb(text); |
||||
|
} |
||||
|
|
||||
|
logger.debug("[ClipboardService] Copy successful", { |
||||
|
platform, |
||||
|
timestamp: new Date().toISOString(), |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
logger.error("[ClipboardService] Copy failed:", { |
||||
|
error: error instanceof Error ? error.message : String(error), |
||||
|
platform, |
||||
|
timestamp: new Date().toISOString(), |
||||
|
}); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Copy text using native Capacitor clipboard API |
||||
|
* |
||||
|
* @param text - The text to copy |
||||
|
* @returns Promise that resolves when copy is complete |
||||
|
*/ |
||||
|
private async copyNative(text: string): Promise<void> { |
||||
|
try { |
||||
|
await Clipboard.write({ |
||||
|
string: text, |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
logger.error("[ClipboardService] Native copy failed:", { |
||||
|
error: error instanceof Error ? error.message : String(error), |
||||
|
timestamp: new Date().toISOString(), |
||||
|
}); |
||||
|
throw new Error( |
||||
|
`Native clipboard copy failed: ${error instanceof Error ? error.message : String(error)}`, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Copy text using web clipboard API with fallback |
||||
|
* |
||||
|
* @param text - The text to copy |
||||
|
* @returns Promise that resolves when copy is complete |
||||
|
*/ |
||||
|
private async copyWeb(text: string): Promise<void> { |
||||
|
try { |
||||
|
// Try VueUse clipboard first (handles some edge cases)
|
||||
|
const { copy } = useClipboard(); |
||||
|
await copy(text); |
||||
|
} catch (error) { |
||||
|
logger.warn( |
||||
|
"[ClipboardService] VueUse clipboard failed, trying native API:", |
||||
|
{ |
||||
|
error: error instanceof Error ? error.message : String(error), |
||||
|
timestamp: new Date().toISOString(), |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
// Fallback to native navigator.clipboard
|
||||
|
if (navigator.clipboard && navigator.clipboard.writeText) { |
||||
|
await navigator.clipboard.writeText(text); |
||||
|
} else { |
||||
|
throw new Error("Clipboard API not supported in this browser"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Read text from clipboard (platform-specific) |
||||
|
* |
||||
|
* @returns Promise that resolves to the clipboard text |
||||
|
* @throws Error if read operation fails |
||||
|
*/ |
||||
|
public async readFromClipboard(): Promise<string> { |
||||
|
const platform = Capacitor.getPlatform(); |
||||
|
const isNative = Capacitor.isNativePlatform(); |
||||
|
|
||||
|
try { |
||||
|
if (isNative && (platform === "ios" || platform === "android")) { |
||||
|
// Use native Capacitor clipboard for mobile platforms
|
||||
|
const result = await Clipboard.read(); |
||||
|
return result.value || ""; |
||||
|
} else { |
||||
|
// Use web clipboard API for web/desktop platforms
|
||||
|
if (navigator.clipboard && navigator.clipboard.readText) { |
||||
|
return await navigator.clipboard.readText(); |
||||
|
} else { |
||||
|
throw new Error("Clipboard read API not supported in this browser"); |
||||
|
} |
||||
|
} |
||||
|
} catch (error) { |
||||
|
logger.error("[ClipboardService] Read from clipboard failed:", { |
||||
|
error: error instanceof Error ? error.message : String(error), |
||||
|
platform, |
||||
|
timestamp: new Date().toISOString(), |
||||
|
}); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if clipboard is supported on current platform |
||||
|
* |
||||
|
* @returns boolean indicating if clipboard is supported |
||||
|
*/ |
||||
|
public isSupported(): boolean { |
||||
|
const platform = Capacitor.getPlatform(); |
||||
|
const isNative = Capacitor.isNativePlatform(); |
||||
|
|
||||
|
if (isNative && (platform === "ios" || platform === "android")) { |
||||
|
return true; // Capacitor clipboard should work on native platforms
|
||||
|
} |
||||
|
|
||||
|
// Check web clipboard support
|
||||
|
return !!(navigator.clipboard && navigator.clipboard.writeText); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convenience function to copy text to clipboard |
||||
|
* Uses the singleton ClipboardService instance |
||||
|
* |
||||
|
* @param text - The text to copy to clipboard |
||||
|
* @returns Promise that resolves when copy is complete |
||||
|
*/ |
||||
|
export async function copyToClipboard(text: string): Promise<void> { |
||||
|
return ClipboardService.getInstance().copyToClipboard(text); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convenience function to read text from clipboard |
||||
|
* Uses the singleton ClipboardService instance |
||||
|
* |
||||
|
* @returns Promise that resolves to the clipboard text |
||||
|
*/ |
||||
|
export async function readFromClipboard(): Promise<string> { |
||||
|
return ClipboardService.getInstance().readFromClipboard(); |
||||
|
} |
@ -0,0 +1,226 @@ |
|||||
|
/** |
||||
|
* Safe Area Inset Injection for Android WebView |
||||
|
* |
||||
|
* This script injects safe area inset values into CSS environment variables |
||||
|
* when running in Android WebView, since Android doesn't natively support |
||||
|
* CSS env(safe-area-inset-*) variables like iOS does. |
||||
|
*/ |
||||
|
|
||||
|
// Check if we're running in Android WebView with Capacitor
|
||||
|
const isAndroidWebView = () => { |
||||
|
// Check if we're on iOS - if so, skip this script entirely
|
||||
|
const isIOS = |
||||
|
/iPad|iPhone|iPod/.test(navigator.userAgent) || |
||||
|
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1); |
||||
|
|
||||
|
if (isIOS) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Check if we're on Android
|
||||
|
const isAndroid = /Android/.test(navigator.userAgent); |
||||
|
|
||||
|
// Check if we have Capacitor (required for Android WebView)
|
||||
|
const hasCapacitor = window.Capacitor !== undefined; |
||||
|
|
||||
|
// Only run on Android with Capacitor
|
||||
|
return isAndroid && hasCapacitor; |
||||
|
}; |
||||
|
|
||||
|
// Wait for Capacitor to be available
|
||||
|
const waitForCapacitor = () => { |
||||
|
return new Promise((resolve) => { |
||||
|
if (window.Capacitor) { |
||||
|
resolve(window.Capacitor); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Wait for Capacitor to be available
|
||||
|
const checkCapacitor = () => { |
||||
|
if (window.Capacitor) { |
||||
|
resolve(window.Capacitor); |
||||
|
} else { |
||||
|
setTimeout(checkCapacitor, 100); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
checkCapacitor(); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
// Inject safe area inset values into CSS custom properties
|
||||
|
const injectSafeAreaInsets = async () => { |
||||
|
try { |
||||
|
// Wait for Capacitor to be available
|
||||
|
const Capacitor = await waitForCapacitor(); |
||||
|
|
||||
|
// Try to get safe area insets using StatusBar plugin (which is already available)
|
||||
|
|
||||
|
let top = 0, |
||||
|
bottom = 0, |
||||
|
left = 0, |
||||
|
right = 0; |
||||
|
|
||||
|
try { |
||||
|
// Use StatusBar plugin to get status bar height
|
||||
|
if (Capacitor.Plugins.StatusBar) { |
||||
|
const statusBarInfo = await Capacitor.Plugins.StatusBar.getInfo(); |
||||
|
// Status bar height is typically the top safe area inset
|
||||
|
top = statusBarInfo.overlays ? 0 : statusBarInfo.height || 0; |
||||
|
} |
||||
|
} catch (error) { |
||||
|
// Status bar info not available, will use fallback
|
||||
|
} |
||||
|
|
||||
|
// Detect navigation bar and gesture bar heights
|
||||
|
const detectNavigationBar = () => { |
||||
|
const screenHeight = window.screen.height; |
||||
|
const screenWidth = window.screen.width; |
||||
|
const windowHeight = window.innerHeight; |
||||
|
const devicePixelRatio = window.devicePixelRatio || 1; |
||||
|
|
||||
|
// Calculate navigation bar height
|
||||
|
let navBarHeight = 0; |
||||
|
|
||||
|
// Method 1: Direct comparison (most reliable)
|
||||
|
if (windowHeight < screenHeight) { |
||||
|
navBarHeight = screenHeight - windowHeight; |
||||
|
} |
||||
|
|
||||
|
// Method 2: Check for gesture navigation indicators
|
||||
|
if (navBarHeight === 0) { |
||||
|
// Look for common gesture navigation patterns
|
||||
|
const isTallDevice = screenHeight > 2000; |
||||
|
const isModernDevice = screenHeight > 1800; |
||||
|
const hasHighDensity = devicePixelRatio >= 2.5; |
||||
|
|
||||
|
if (isTallDevice && hasHighDensity) { |
||||
|
// Modern gesture-based device
|
||||
|
navBarHeight = 12; // Typical gesture bar height
|
||||
|
} else if (isModernDevice) { |
||||
|
// Modern device with traditional navigation
|
||||
|
navBarHeight = 48; // Traditional navigation bar height
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Method 3: Check visual viewport (more accurate for WebView)
|
||||
|
if (navBarHeight === 0) { |
||||
|
if (window.visualViewport) { |
||||
|
const visualHeight = window.visualViewport.height; |
||||
|
|
||||
|
if (visualHeight < windowHeight) { |
||||
|
navBarHeight = windowHeight - visualHeight; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Method 4: Device-specific estimation based on screen dimensions
|
||||
|
if (navBarHeight === 0) { |
||||
|
// Common Android navigation bar heights in pixels
|
||||
|
const commonNavBarHeights = { |
||||
|
"1080x2400": 48, // Common 1080p devices
|
||||
|
"1440x3200": 64, // QHD devices
|
||||
|
"720x1600": 32, // HD devices
|
||||
|
}; |
||||
|
|
||||
|
const resolution = `${screenWidth}x${screenHeight}`; |
||||
|
const estimatedHeight = commonNavBarHeights[resolution]; |
||||
|
|
||||
|
if (estimatedHeight) { |
||||
|
navBarHeight = estimatedHeight; |
||||
|
} else { |
||||
|
// Fallback: estimate based on screen height
|
||||
|
navBarHeight = screenHeight > 2000 ? 48 : 32; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return navBarHeight; |
||||
|
}; |
||||
|
|
||||
|
// Get navigation bar height
|
||||
|
bottom = detectNavigationBar(); |
||||
|
|
||||
|
// If we still don't have a top value, estimate it
|
||||
|
if (top === 0) { |
||||
|
const screenHeight = window.screen.height; |
||||
|
// Common status bar heights: 24dp (48px) for most devices, 32dp (64px) for some
|
||||
|
top = screenHeight > 1920 ? 64 : 48; |
||||
|
} |
||||
|
|
||||
|
// Left/right safe areas are rare on Android
|
||||
|
left = 0; |
||||
|
right = 0; |
||||
|
|
||||
|
// Create CSS custom properties
|
||||
|
const style = document.createElement("style"); |
||||
|
style.textContent = ` |
||||
|
:root { |
||||
|
--safe-area-inset-top: ${top}px; |
||||
|
--safe-area-inset-bottom: ${bottom}px; |
||||
|
--safe-area-inset-left: ${left}px; |
||||
|
--safe-area-inset-right: ${right}px; |
||||
|
} |
||||
|
`;
|
||||
|
|
||||
|
// Inject the style into the document head
|
||||
|
document.head.appendChild(style); |
||||
|
|
||||
|
// Also set CSS environment variables if supported
|
||||
|
if (CSS.supports("env(safe-area-inset-top)")) { |
||||
|
document.documentElement.style.setProperty( |
||||
|
"--env-safe-area-inset-top", |
||||
|
`${top}px`, |
||||
|
); |
||||
|
document.documentElement.style.setProperty( |
||||
|
"--env-safe-area-inset-bottom", |
||||
|
`${bottom}px`, |
||||
|
); |
||||
|
document.documentElement.style.setProperty( |
||||
|
"--env-safe-area-inset-left", |
||||
|
`${left}px`, |
||||
|
); |
||||
|
document.documentElement.style.setProperty( |
||||
|
"--env-safe-area-inset-right", |
||||
|
`${right}px`, |
||||
|
); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
// Error injecting safe area insets, will use fallback values
|
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// Initialize when DOM is ready
|
||||
|
const initializeSafeArea = () => { |
||||
|
// Check if we should run this script at all
|
||||
|
if (!isAndroidWebView()) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Add a small delay to ensure WebView is fully initialized
|
||||
|
setTimeout(() => { |
||||
|
injectSafeAreaInsets(); |
||||
|
}, 100); |
||||
|
}; |
||||
|
|
||||
|
if (document.readyState === "loading") { |
||||
|
document.addEventListener("DOMContentLoaded", initializeSafeArea); |
||||
|
} else { |
||||
|
initializeSafeArea(); |
||||
|
} |
||||
|
|
||||
|
// Re-inject on orientation change (only on Android)
|
||||
|
window.addEventListener("orientationchange", () => { |
||||
|
if (isAndroidWebView()) { |
||||
|
setTimeout(() => injectSafeAreaInsets(), 100); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// Re-inject on resize (only on Android)
|
||||
|
window.addEventListener("resize", () => { |
||||
|
if (isAndroidWebView()) { |
||||
|
setTimeout(() => injectSafeAreaInsets(), 100); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// Export for use in other modules
|
||||
|
export { injectSafeAreaInsets, isAndroidWebView }; |
Loading…
Reference in new issue