ios: https://github.com/user-attachments/assets/efdfd3e3-239f-4cbf-be1a-7784d1df1851 android: https://github.com/user-attachments/assets/a8e22a5c-60f3-4fb6-8923-b8a786bceb0c ## Overview This PR introduces native iOS UI components, refactors the profile menu, and simplifies the event type creation flow for a more native iOS experience. ## Key Changes ### iOS Native UI Components - **iOS-specific Event Types Page**: Added native Stack.Header, glass UI, and context menus for iOS - **Platform-specific Profile Sheet**: Created separate implementations for iOS (formSheet) and Android/Web (modal) - **Bottom Glass UI Navbar**: Updated bottom navigation bar with glass UI styling for iOS - **Native iOS Alerts**: Replaced custom modals with native `Alert.prompt` for event types and availability pages ### Profile Menu Refactor - Restructured More page with folder layout and native iOS header support - Added profile button to More page header on iOS with glass UI - Added "Copy public page link" and "Roadmap" options to profile sheet - Refactored Header component: removed inline profile modal, now uses route-based navigation - Updated Availability page with New menu and profile button in header - Fixed "My Settings" URL to use `/general` endpoint - Removed "Profile" item from More section menu - Updated tab layout to support `(more)` folder structure ### Simplified Event Type Creation - Reduced event type creation to only require title input - Auto-generate slug from title using slugify utility - Set default duration to 15 minutes - Leave description empty (users can edit later) - Applied to both iOS and Android/Web platforms ### UI Improvements - Updated login button styling: changed background color to pure black (#000000) with white text for better contrast
144 lines
4.1 KiB
TypeScript
144 lines
4.1 KiB
TypeScript
/**
|
|
* React Query Cache Persister
|
|
*
|
|
* Uses the shared storage adapter from utils/storage.ts for cross-platform support.
|
|
*/
|
|
|
|
import type { PersistedClient, Persister } from "@tanstack/react-query-persist-client";
|
|
import { CACHE_CONFIG } from "@/config/cache.config";
|
|
import { safeLogWarn } from "./safeLogger";
|
|
import { generalStorage } from "./storage";
|
|
|
|
// Use the shared general storage adapter for cache persistence
|
|
const storage = generalStorage;
|
|
|
|
/**
|
|
* Create a React Query persister that works across all platforms
|
|
*
|
|
* This persister:
|
|
* - Saves the query cache to platform-appropriate storage
|
|
* - Restores cache on app launch for instant data display
|
|
* - Handles serialization/deserialization of cache data
|
|
* - Respects cache expiration (maxAge)
|
|
*/
|
|
export const createQueryPersister = (): Persister => {
|
|
const storageKey = CACHE_CONFIG.persistence.storageKey;
|
|
const maxAge = CACHE_CONFIG.persistence.maxAge;
|
|
|
|
return {
|
|
/**
|
|
* Persist the client state to storage
|
|
*/
|
|
persistClient: async (client: PersistedClient): Promise<void> => {
|
|
try {
|
|
const serialized = JSON.stringify(client);
|
|
await storage.setItem(storageKey, serialized);
|
|
} catch (error) {
|
|
safeLogWarn("[QueryPersister] Failed to persist client:", error);
|
|
// Fail silently - persistence is a nice-to-have, not critical
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Restore the client state from storage
|
|
*/
|
|
restoreClient: async (): Promise<PersistedClient | undefined> => {
|
|
try {
|
|
const serialized = await storage.getItem(storageKey);
|
|
if (!serialized) {
|
|
return undefined;
|
|
}
|
|
|
|
const client = JSON.parse(serialized) as PersistedClient;
|
|
|
|
// Validate timestamp exists and is a valid number
|
|
if (typeof client.timestamp !== "number" || Number.isNaN(client.timestamp)) {
|
|
safeLogWarn("[QueryPersister] Invalid or missing timestamp, discarding cache");
|
|
await storage.removeItem(storageKey);
|
|
return undefined;
|
|
}
|
|
|
|
// Check if the persisted cache has expired
|
|
const persistedAt = client.timestamp;
|
|
const now = Date.now();
|
|
if (now - persistedAt > maxAge) {
|
|
// Cache is too old, discard it
|
|
await storage.removeItem(storageKey);
|
|
return undefined;
|
|
}
|
|
|
|
return client;
|
|
} catch (error) {
|
|
safeLogWarn("[QueryPersister] Failed to restore client:", error);
|
|
// If restoration fails, start fresh
|
|
return undefined;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Remove the persisted client state
|
|
*/
|
|
removeClient: async (): Promise<void> => {
|
|
try {
|
|
await storage.removeItem(storageKey);
|
|
} catch (error) {
|
|
safeLogWarn("[QueryPersister] Failed to remove client:", error);
|
|
}
|
|
},
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Export the storage adapter for potential direct use
|
|
*/
|
|
export { storage };
|
|
|
|
/**
|
|
* Utility to clear all query cache from storage
|
|
* Useful for logout or cache reset scenarios
|
|
*/
|
|
export const clearQueryCache = async (): Promise<void> => {
|
|
try {
|
|
await storage.removeItem(CACHE_CONFIG.persistence.storageKey);
|
|
} catch (error) {
|
|
safeLogWarn("[QueryPersister] Failed to clear cache:", error);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get cache metadata (for debugging/status display)
|
|
*/
|
|
export const getCacheMetadata = async (): Promise<{
|
|
exists: boolean;
|
|
timestamp?: number;
|
|
age?: number;
|
|
isExpired?: boolean;
|
|
} | null> => {
|
|
try {
|
|
const serialized = await storage.getItem(CACHE_CONFIG.persistence.storageKey);
|
|
if (!serialized) {
|
|
return { exists: false };
|
|
}
|
|
|
|
const client = JSON.parse(serialized) as PersistedClient;
|
|
|
|
// Validate timestamp exists and is a valid number
|
|
if (typeof client.timestamp !== "number" || Number.isNaN(client.timestamp)) {
|
|
return { exists: true, isExpired: true }; // Treat invalid timestamp as expired
|
|
}
|
|
|
|
const now = Date.now();
|
|
const age = now - client.timestamp;
|
|
|
|
return {
|
|
exists: true,
|
|
timestamp: client.timestamp,
|
|
age,
|
|
isExpired: age > CACHE_CONFIG.persistence.maxAge,
|
|
};
|
|
} catch (error) {
|
|
safeLogWarn("[QueryPersister] Failed to get cache metadata:", error);
|
|
return null;
|
|
}
|
|
};
|