Files
calendar/companion/utils/queryPersister.ts
T
Dhairyashil ShindeandGitHub 49ae10149f feat(companion): iOS native UI components and simplified event type creation (#26240)
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
2025-12-28 18:48:48 -03:00

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;
}
};