Update mobile app UI with premium design system

- Add design guidelines to CLAUDE.md
- Redesign navigation with boutique styling
- Add video support to chat with compression
- Update matches/dares/quiz screens with gold/rose accents
- Improve packs browsing with category tabs
- Add SwipeCardPremium component
- Update theme with premium color palette
- Add video cleanup edge function and migration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jamie BruntonandClaude Opus 4.5 committed 2026-01-01 20:26:33 +00:00
1 parent feb7657562
commit cac7a2a31d
31 files changed
+6638 -1827

No files matched your search

+139
View File
@@ -1,4 +1,6 @@
import { supabase } from './supabase';
import * as FileSystem from 'expo-file-system';
import { Platform } from 'react-native';
// Cache for signed URLs with expiration tracking
interface CachedUrl {
@@ -8,6 +10,12 @@ interface CachedUrl {
const signedUrlCache = new Map<string, CachedUrl>();
// In-memory tracking of videos currently being cached to prevent duplicate downloads
const videoCacheInProgress = new Set<string>();
// Video cache directory
const VIDEO_CACHE_DIR = `${FileSystem.cacheDirectory}video-cache/`;
// Buffer time before expiration to refresh URL (5 minutes)
const EXPIRATION_BUFFER_MS = 5 * 60 * 1000;
// Default signed URL expiry (1 hour)
@@ -93,3 +101,134 @@ export async function prefetchSignedUrls(
mediaPaths.map(path => getCachedSignedUrl(getStoragePath(path), bucket))
);
}
/**
* Ensure the video cache directory exists.
*/
async function ensureVideoCacheDir(): Promise<void> {
if (Platform.OS === 'web') return;
const dirInfo = await FileSystem.getInfoAsync(VIDEO_CACHE_DIR);
if (!dirInfo.exists) {
await FileSystem.makeDirectoryAsync(VIDEO_CACHE_DIR, { intermediates: true });
}
}
/**
* Generate a safe filename from a storage path.
*/
function getVideoCacheFilename(storagePath: string): string {
// Replace slashes and other unsafe chars with underscores
return storagePath.replace(/[\/\\:*?"<>|]/g, '_');
}
/**
* Get the local cache path for a video.
*/
export function getVideoCachePath(storagePath: string): string {
return `${VIDEO_CACHE_DIR}${getVideoCacheFilename(storagePath)}`;
}
/**
* Check if a video is cached locally.
* Returns the local file URI if cached, null otherwise.
*/
export async function getVideoCachedUri(storagePath: string): Promise<string | null> {
if (Platform.OS === 'web') return null;
const cachePath = getVideoCachePath(storagePath);
const fileInfo = await FileSystem.getInfoAsync(cachePath);
if (fileInfo.exists) {
return cachePath;
}
return null;
}
/**
* Cache a video to local storage from a signed URL.
* Returns the local file URI on success, null on failure.
*/
export async function cacheVideo(
storagePath: string,
signedUrl: string
): Promise<string | null> {
if (Platform.OS === 'web') return null;
// Check if already cached
const existingCache = await getVideoCachedUri(storagePath);
if (existingCache) {
return existingCache;
}
// Check if caching is already in progress for this video
if (videoCacheInProgress.has(storagePath)) {
return null;
}
try {
videoCacheInProgress.add(storagePath);
await ensureVideoCacheDir();
const cachePath = getVideoCachePath(storagePath);
// Download the video file
const downloadResult = await FileSystem.downloadAsync(signedUrl, cachePath);
if (downloadResult.status === 200) {
console.log('Video cached successfully:', storagePath);
return cachePath;
} else {
console.warn('Video cache download failed with status:', downloadResult.status);
return null;
}
} catch (error) {
console.error('Failed to cache video:', error);
return null;
} finally {
videoCacheInProgress.delete(storagePath);
}
}
/**
* Clear all cached videos.
*/
export async function clearVideoCache(): Promise<void> {
if (Platform.OS === 'web') return;
try {
const dirInfo = await FileSystem.getInfoAsync(VIDEO_CACHE_DIR);
if (dirInfo.exists) {
await FileSystem.deleteAsync(VIDEO_CACHE_DIR, { idempotent: true });
}
} catch (error) {
console.error('Failed to clear video cache:', error);
}
}
/**
* Get the size of the video cache in bytes.
*/
export async function getVideoCacheSize(): Promise<number> {
if (Platform.OS === 'web') return 0;
try {
const dirInfo = await FileSystem.getInfoAsync(VIDEO_CACHE_DIR);
if (!dirInfo.exists) return 0;
const files = await FileSystem.readDirectoryAsync(VIDEO_CACHE_DIR);
let totalSize = 0;
for (const file of files) {
const fileInfo = await FileSystem.getInfoAsync(`${VIDEO_CACHE_DIR}${file}`);
if (fileInfo.exists && 'size' in fileInfo) {
totalSize += fileInfo.size || 0;
}
}
return totalSize;
} catch (error) {
console.error('Failed to get video cache size:', error);
return 0;
}
}