refactor: restructure

monorepo with apps/ and services/
  directories
This commit is contained in:
Wuesteon 2025-11-26 03:03:24 +01:00
parent 25824ed0ac
commit ff80aeec1f
4062 changed files with 2592 additions and 1278 deletions

View file

@ -0,0 +1,96 @@
/**
* BlurHash utility functions
*
* BlurHash is a compact representation of an image placeholder that can be
* decoded into a blurred version of the original image.
*/
import { encode } from 'blurhash';
/**
* Generate a BlurHash from an image URL
*
* @param imageUrl - URL of the image to generate BlurHash for
* @param componentX - Number of horizontal components (default: 4)
* @param componentY - Number of vertical components (default: 3)
* @returns BlurHash string or null on error
*/
export async function generateBlurHash(
imageUrl: string,
componentX: number = 4,
componentY: number = 3
): Promise<string | null> {
try {
// Download the image
const response = await fetch(imageUrl);
if (!response.ok) {
console.error('Failed to fetch image for BlurHash generation');
return null;
}
const blob = await response.blob();
// Create image bitmap for pixel data extraction
const imageBitmap = await createImageBitmap(blob);
// Create canvas to get pixel data
const canvas = document.createElement('canvas');
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
const context = canvas.getContext('2d');
if (!context) {
console.error('Could not get canvas context');
return null;
}
context.drawImage(imageBitmap, 0, 0);
// Get pixel data
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
// Generate BlurHash
const blurHash = encode(
imageData.data,
imageData.width,
imageData.height,
componentX,
componentY
);
return blurHash;
} catch (error) {
console.error('Error generating BlurHash:', error);
return null;
}
}
/**
* Generate BlurHash from Image Uint8Array (for Edge Functions)
*
* This is a server-side compatible version that works with Deno
*/
export async function generateBlurHashFromBuffer(
imageBuffer: Uint8Array,
width: number,
height: number,
componentX: number = 4,
componentY: number = 3
): Promise<string | null> {
try {
// This would need an image decoding library in Deno
// For now, we return a placeholder
// TODO: Implement proper image decoding in Deno environment
console.log('BlurHash generation from buffer not yet implemented for Deno');
return null;
} catch (error) {
console.error('Error generating BlurHash from buffer:', error);
return null;
}
}
/**
* Default BlurHash fallback
* A neutral gray blur that works well for most images
*/
export const DEFAULT_BLURHASH = 'L5H2EC=PM+yV0g-mq.wG9c010J}I';

View file

@ -0,0 +1,66 @@
/**
* Image utility functions for handling Supabase Storage transformations
*/
export type ThumbnailSize = 'tiny' | 'small' | 'medium' | 'full';
/**
* Get the appropriate thumbnail URL based on the view mode or size requirement
*
* Supabase Storage Transformations: https://supabase.com/docs/guides/storage/serving/image-transformations
* Format: {storage-url}/render/image/authenticated/{bucket}/{path}?width={w}&height={h}
*/
export function getThumbnailUrl(
publicUrl: string | null,
size: ThumbnailSize = 'medium'
): string | null {
if (!publicUrl) return null;
// If it's already a full URL with transformations, return as is
if (publicUrl.includes('?width=') || publicUrl.includes('&width=')) {
return publicUrl;
}
// Determine dimensions based on size
const dimensions: Record<ThumbnailSize, number> = {
tiny: 100, // For grid5 view
small: 200, // For grid3 view
medium: 400, // For single view
full: 0, // No transformation, use original
};
const targetSize = dimensions[size];
// If full resolution requested, return original URL
if (targetSize === 0) {
return publicUrl;
}
// Parse the Supabase Storage URL
// Expected format: https://{project}.supabase.co/storage/v1/object/public/{bucket}/{path}
const url = new URL(publicUrl);
// Add transformation parameters
url.searchParams.set('width', targetSize.toString());
url.searchParams.set('height', targetSize.toString());
url.searchParams.set('resize', 'cover'); // Crop to fill the dimensions
url.searchParams.set('quality', '80'); // Good balance between quality and file size
return url.toString();
}
/**
* Get thumbnail size based on view mode
*/
export function getSizeForViewMode(viewMode: 'single' | 'grid3' | 'grid5'): ThumbnailSize {
switch (viewMode) {
case 'grid5':
return 'tiny';
case 'grid3':
return 'small';
case 'single':
return 'medium';
default:
return 'medium';
}
}

View file

@ -0,0 +1,91 @@
/**
* Centralized logging utility
* - Development: Logs to console
* - Production: Can be integrated with Sentry, LogRocket, etc.
*/
const isDevelopment = __DEV__;
export const logger = {
/**
* Log debug information (only in development)
*/
debug: (...args: any[]) => {
if (isDevelopment) {
console.log('[DEBUG]', ...args);
}
},
/**
* Log general information
*/
info: (...args: any[]) => {
if (isDevelopment) {
console.log('[INFO]', ...args);
}
},
/**
* Log warnings (always logged)
*/
warn: (...args: any[]) => {
console.warn('[WARN]', ...args);
// TODO: Send to error tracking service in production
},
/**
* Log errors (always logged)
*/
error: (...args: any[]) => {
console.error('[ERROR]', ...args);
// TODO: Send to error tracking service (Sentry, etc.)
},
/**
* Log success messages (only in development)
*/
success: (...args: any[]) => {
if (isDevelopment) {
console.log('[SUCCESS] ✅', ...args);
}
},
};
/**
* Performance logger for measuring execution time
*/
export const perfLogger = {
start: (label: string) => {
if (isDevelopment) {
console.time(label);
}
},
end: (label: string) => {
if (isDevelopment) {
console.timeEnd(label);
}
},
};
/**
* Network request logger
*/
export const networkLogger = {
request: (url: string, method: string, body?: any) => {
if (isDevelopment) {
console.log(`[NETWORK] ${method} ${url}`, body ? { body } : '');
}
},
response: (url: string, status: number, data?: any) => {
if (isDevelopment) {
console.log(`[NETWORK] Response ${status} from ${url}`, data ? { data } : '');
}
},
error: (url: string, error: any) => {
console.error(`[NETWORK] Error from ${url}`, error);
// TODO: Send to error tracking
},
};

View file

@ -0,0 +1,9 @@
// Polyfills for web platform
// This file handles compatibility issues when running in web browser
// Export a mock for @supabase/node-fetch that uses browser fetch
const nodeFetch = (...args) => fetch(...args);
nodeFetch.default = nodeFetch;
export default nodeFetch;
export { nodeFetch as fetch };

View file

@ -0,0 +1,56 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createClient } from '@supabase/supabase-js';
import { Platform } from 'react-native';
import type { Database } from '@picture/shared/types';
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL || 'https://mjuvnnjxwfwlmxjsgkqu.supabase.co';
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1qdXZubmp4d2Z3bG14anNna3F1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTYyNTg5NTUsImV4cCI6MjA3MTgzNDk1NX0.EeOKzyPnZ42zpFl7oi54qDcAZSW-XGoB0tSNwUiX9GU';
// Create a storage adapter that works for both web and mobile
const createStorage = () => {
if (Platform.OS === 'web') {
// For web, use a simple localStorage wrapper
return {
getItem: async (key: string) => {
try {
if (typeof window !== 'undefined' && window.localStorage) {
const item = window.localStorage.getItem(key);
return item;
}
} catch (error) {
console.error('Error getting item from localStorage:', error);
}
return null;
},
setItem: async (key: string, value: string) => {
try {
if (typeof window !== 'undefined' && window.localStorage) {
window.localStorage.setItem(key, value);
}
} catch (error) {
console.error('Error setting item in localStorage:', error);
}
},
removeItem: async (key: string) => {
try {
if (typeof window !== 'undefined' && window.localStorage) {
window.localStorage.removeItem(key);
}
} catch (error) {
console.error('Error removing item from localStorage:', error);
}
},
};
}
// For mobile, use AsyncStorage
return AsyncStorage;
};
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey, {
auth: {
storage: createStorage(),
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: Platform.OS === 'web',
},
});