From b4c8a2f3036ce34d5f46a6ca8195ab20d5b1639d Mon Sep 17 00:00:00 2001 From: Jamie Brunton Date: Thu, 1 Jan 2026 16:09:01 +0000 Subject: [PATCH] Update --- .../src/pages/activity/UserActivityPage.tsx | 670 ++++++++++++++++++ apps/mobile/app.json | 4 +- etc/icons/google/logo-120x120-white.png | Bin 0 -> 14012 bytes scripts/build-android-local.sh | 141 ++++ scripts/setup-wsl-android.sh | 94 +++ 5 files changed, 908 insertions(+), 1 deletion(-) create mode 100644 apps/admin/src/pages/activity/UserActivityPage.tsx create mode 100644 etc/icons/google/logo-120x120-white.png create mode 100644 scripts/build-android-local.sh create mode 100644 scripts/setup-wsl-android.sh diff --git a/apps/admin/src/pages/activity/UserActivityPage.tsx b/apps/admin/src/pages/activity/UserActivityPage.tsx new file mode 100644 index 0000000..128c566 --- /dev/null +++ b/apps/admin/src/pages/activity/UserActivityPage.tsx @@ -0,0 +1,670 @@ +import { useState, useEffect, useCallback } from 'react'; +import { Link } from 'react-router-dom'; +import { supabase } from '@/config'; +import { Badge } from '@/components/ui/badge'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription'; +import { RealtimeStatusIndicator } from '@/components/RealtimeStatusIndicator'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from '@/components/ui/tabs'; +import { formatDistanceToNow, format } from 'date-fns'; +import { toast } from 'sonner'; +import { + Activity, + ThumbsUp, + ThumbsDown, + Minus, + Heart, + MessageCircle, + UserPlus, + Image as ImageIcon, +} from 'lucide-react'; + +// Types +interface ResponseActivity { + id: string; + user_id: string; + answer: 'yes' | 'no' | 'maybe'; + created_at: string; + profile: { + id: string; + name: string | null; + email: string | null; + } | null; + question: { + id: string; + text: string; + pack: { + name: string; + } | null; + } | null; +} + +interface MatchActivity { + id: string; + couple_id: string; + match_type: 'yes_yes' | 'yes_maybe' | 'maybe_maybe'; + is_new: boolean; + created_at: string; + question: { + id: string; + text: string; + } | null; + coupleProfiles?: Array<{ + id: string; + name: string | null; + email: string | null; + }>; +} + +interface MessageActivity { + id: string; + user_id: string; + content: string | null; + media_path: string | null; + created_at: string; + profile: { + id: string; + name: string | null; + email: string | null; + } | null; + match: { + id: string; + question: { + text: string; + } | null; + } | null; +} + +interface SignupActivity { + id: string; + name: string | null; + email: string | null; + avatar_url: string | null; + created_at: string; + onboarding_completed: boolean | null; + couple_id: string | null; +} + +type ActivityTab = 'responses' | 'matches' | 'messages' | 'signups'; + +// Answer icons +const answerConfig = { + yes: { icon: , color: 'text-green-500', bg: 'bg-green-500/10' }, + no: { icon: , color: 'text-red-500', bg: 'bg-red-500/10' }, + maybe: { icon: , color: 'text-yellow-500', bg: 'bg-yellow-500/10' }, +}; + +const matchTypeLabels = { + yes_yes: { label: 'Both Yes!', variant: 'default' as const }, + yes_maybe: { label: 'Yes + Maybe', variant: 'secondary' as const }, + maybe_maybe: { label: 'Both Maybe', variant: 'outline' as const }, +}; + +export function UserActivityPage() { + const [activeTab, setActiveTab] = useState('responses'); + const [responses, setResponses] = useState([]); + const [matches, setMatches] = useState([]); + const [messages, setMessages] = useState([]); + const [signups, setSignups] = useState([]); + const [loading, setLoading] = useState({ + responses: true, + matches: true, + messages: true, + signups: true, + }); + + // Fetch responses + const fetchResponses = useCallback(async () => { + setLoading(prev => ({ ...prev, responses: true })); + try { + const { data, error } = await supabase + .from('responses') + .select(` + id, user_id, answer, created_at, + profile:profiles!responses_user_id_fkey(id, name, email), + question:questions( + id, text, + pack:question_packs(name) + ) + `) + .order('created_at', { ascending: false }) + .limit(50); + + if (error) throw error; + + // Transform data to match our types (Supabase returns nested objects, not arrays for 1:1 relations) + const transformed: ResponseActivity[] = (data || []).map((item: any) => ({ + id: item.id, + user_id: item.user_id, + answer: item.answer, + created_at: item.created_at, + profile: item.profile, + question: item.question ? { + id: item.question.id, + text: item.question.text, + pack: item.question.pack, + } : null, + })); + setResponses(transformed); + } catch (error) { + console.error('Failed to load responses:', error); + toast.error("Failed to load responses"); + } finally { + setLoading(prev => ({ ...prev, responses: false })); + } + }, []); + + // Fetch matches with couple profiles + const fetchMatches = useCallback(async () => { + setLoading(prev => ({ ...prev, matches: true })); + try { + const { data: matchesData, error: matchesError } = await supabase + .from('matches') + .select(` + id, couple_id, match_type, is_new, created_at, + question:questions(id, text) + `) + .order('created_at', { ascending: false }) + .limit(50); + + if (matchesError) throw matchesError; + + // Get unique couple IDs and fetch profiles + const coupleIds = [...new Set((matchesData || []).map((m: any) => m.couple_id))]; + if (coupleIds.length > 0) { + const { data: profiles, error: profilesError } = await supabase + .from('profiles') + .select('id, name, email, couple_id') + .in('couple_id', coupleIds); + + if (profilesError) throw profilesError; + + // Attach profiles to matches + const matchesWithProfiles: MatchActivity[] = (matchesData || []).map((match: any) => ({ + id: match.id, + couple_id: match.couple_id, + match_type: match.match_type, + is_new: match.is_new, + created_at: match.created_at, + question: match.question, + coupleProfiles: (profiles || []).filter((p: any) => p.couple_id === match.couple_id), + })); + setMatches(matchesWithProfiles); + } else { + const transformed: MatchActivity[] = (matchesData || []).map((match: any) => ({ + id: match.id, + couple_id: match.couple_id, + match_type: match.match_type, + is_new: match.is_new, + created_at: match.created_at, + question: match.question, + coupleProfiles: [], + })); + setMatches(transformed); + } + } catch (error) { + console.error('Failed to load matches:', error); + toast.error("Failed to load matches"); + } finally { + setLoading(prev => ({ ...prev, matches: false })); + } + }, []); + + // Fetch messages + const fetchMessages = useCallback(async () => { + setLoading(prev => ({ ...prev, messages: true })); + try { + // Fetch messages with match data + const { data: messagesData, error: messagesError } = await supabase + .from('messages') + .select(` + id, user_id, content, media_path, created_at, + match:matches( + id, + question:questions(text) + ) + `) + .order('created_at', { ascending: false }) + .limit(50); + + if (messagesError) throw messagesError; + + // Get unique user IDs and fetch profiles + const userIds = [...new Set((messagesData || []).map((m: any) => m.user_id))]; + let profilesMap: Record = {}; + + if (userIds.length > 0) { + const { data: profiles, error: profilesError } = await supabase + .from('profiles') + .select('id, name, email') + .in('id', userIds); + + if (profilesError) throw profilesError; + + profilesMap = (profiles || []).reduce((acc: any, p: any) => { + acc[p.id] = p; + return acc; + }, {}); + } + + // Transform data to match our types + const transformed: MessageActivity[] = (messagesData || []).map((item: any) => ({ + id: item.id, + user_id: item.user_id, + content: item.content, + media_path: item.media_path, + created_at: item.created_at, + profile: profilesMap[item.user_id] || null, + match: item.match ? { + id: item.match.id, + question: item.match.question, + } : null, + })); + setMessages(transformed); + } catch (error) { + console.error('Failed to load messages:', error); + toast.error("Failed to load messages"); + } finally { + setLoading(prev => ({ ...prev, messages: false })); + } + }, []); + + // Fetch signups + const fetchSignups = useCallback(async () => { + setLoading(prev => ({ ...prev, signups: true })); + try { + const { data, error } = await supabase + .from('profiles') + .select('id, name, email, avatar_url, created_at, onboarding_completed, couple_id') + .order('created_at', { ascending: false }) + .limit(50); + + if (error) throw error; + setSignups(data || []); + } catch (error) { + console.error('Failed to load signups:', error); + toast.error("Failed to load signups"); + } finally { + setLoading(prev => ({ ...prev, signups: false })); + } + }, []); + + // Real-time subscriptions + const { status: responsesStatus } = useRealtimeSubscription({ + table: 'responses', + onInsert: useCallback(() => fetchResponses(), [fetchResponses]), + insertToast: { enabled: true, message: () => 'New response', type: 'info' }, + }); + + const { status: matchesStatus } = useRealtimeSubscription({ + table: 'matches', + onInsert: useCallback(() => fetchMatches(), [fetchMatches]), + insertToast: { enabled: true, message: () => 'New match!', type: 'success' }, + }); + + const { status: messagesStatus } = useRealtimeSubscription({ + table: 'messages', + onInsert: useCallback(() => fetchMessages(), [fetchMessages]), + insertToast: { enabled: true, message: () => 'New message', type: 'info' }, + }); + + const { status: signupsStatus } = useRealtimeSubscription({ + table: 'profiles', + onInsert: useCallback(() => fetchSignups(), [fetchSignups]), + insertToast: { enabled: true, message: () => 'New user signed up!', type: 'success' }, + }); + + // Fetch all data on mount + useEffect(() => { + fetchResponses(); + fetchMatches(); + fetchMessages(); + fetchSignups(); + }, [fetchResponses, fetchMatches, fetchMessages, fetchSignups]); + + // Get current realtime status based on active tab + const getCurrentRealtimeStatus = () => { + switch (activeTab) { + case 'responses': return responsesStatus; + case 'matches': return matchesStatus; + case 'messages': return messagesStatus; + case 'signups': return signupsStatus; + } + }; + + // Render user link + const renderUserLink = (profile: { id: string; name: string | null; email: string | null } | null) => { + if (!profile) return Unknown User; + return ( + + {profile.name || profile.email || 'Unknown'} + + ); + }; + + // Render answer badge + const renderAnswerBadge = (answer: 'yes' | 'no' | 'maybe') => { + const config = answerConfig[answer]; + return ( + + {config.icon} + {answer} + + ); + }; + + return ( +
+ {/* Header */} +
+
+
+

+ + User Activity +

+ +
+

+ Monitor recent user activity across the platform +

+
+
+ + {/* Tabs */} + setActiveTab(v as ActivityTab)}> + + + + Responses + + + + Matches + + + + Messages + + + + Signups + + + + {/* Responses Tab */} + +
+ + + + User + Question + Pack + Answer + Time + + + + {loading.responses ? ( + + +
+
+
+ ) : responses.length === 0 ? ( + + + No responses yet. + + + ) : ( + responses.map((item) => ( + + {renderUserLink(item.profile)} + + + {item.question?.text || 'Unknown question'} + + + + + {item.question?.pack?.name || 'Unknown'} + + + {renderAnswerBadge(item.answer)} + + {formatDistanceToNow(new Date(item.created_at), { addSuffix: true })} + + + )) + )} +
+
+
+
+ + {/* Matches Tab */} + +
+ + + + Couple + Question + Match Type + Status + Time + + + + {loading.matches ? ( + + +
+
+
+ ) : matches.length === 0 ? ( + + + No matches yet. + + + ) : ( + matches.map((item) => ( + + +
+ {item.coupleProfiles && item.coupleProfiles.length > 0 ? ( + <> + {renderUserLink(item.coupleProfiles[0])} + {item.coupleProfiles.length > 1 && ( + <> + & + {renderUserLink(item.coupleProfiles[1])} + + )} + + ) : ( + Unknown couple + )} +
+
+ + + {item.question?.text || 'Unknown question'} + + + + + {matchTypeLabels[item.match_type]?.label || item.match_type} + + + + {item.is_new ? ( + New + ) : ( + Viewed + )} + + + {formatDistanceToNow(new Date(item.created_at), { addSuffix: true })} + +
+ )) + )} +
+
+
+
+ + {/* Messages Tab */} + +
+ + + + User + Message + Context + Time + + + + {loading.messages ? ( + + +
+
+
+ ) : messages.length === 0 ? ( + + + No messages yet. + + + ) : ( + messages.map((item) => ( + + {renderUserLink(item.profile)} + + {item.media_path ? ( + + + [Media] + + ) : ( + + {item.content || Empty message} + + )} + + + + {item.match?.question?.text || 'Unknown match'} + + + + {formatDistanceToNow(new Date(item.created_at), { addSuffix: true })} + + + )) + )} +
+
+
+
+ + {/* Signups Tab */} + +
+ + + + User + Email + Status + Paired + Joined + + + + {loading.signups ? ( + + +
+
+
+ ) : signups.length === 0 ? ( + + + No signups yet. + + + ) : ( + signups.map((item) => ( + + + + + + + {(item.name || item.email || '?').charAt(0).toUpperCase()} + + + + {item.name || 'No name'} + + + + + {item.email || 'No email'} + + + {item.onboarding_completed ? ( + Onboarded + ) : ( + Pending + )} + + + {item.couple_id ? ( + Paired + ) : ( + Single + )} + + + {format(new Date(item.created_at), 'MMM d, yyyy')} + + + )) + )} +
+
+
+
+
+
+ ); +} diff --git a/apps/mobile/app.json b/apps/mobile/app.json index b114e28..e0bf46d 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -32,7 +32,9 @@ "package": "com.sauci.app", "permissions": [ "RECEIVE_BOOT_COMPLETED", - "VIBRATE" + "VIBRATE", + "android.permission.USE_BIOMETRIC", + "android.permission.USE_FINGERPRINT" ] }, "web": { diff --git a/etc/icons/google/logo-120x120-white.png b/etc/icons/google/logo-120x120-white.png new file mode 100644 index 0000000000000000000000000000000000000000..18b55b414d4e53e16199b35b371c3abc83e53938 GIT binary patch literal 14012 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 literal 0 HcmV?d00001 diff --git a/scripts/build-android-local.sh b/scripts/build-android-local.sh new file mode 100644 index 0000000..896e2d4 --- /dev/null +++ b/scripts/build-android-local.sh @@ -0,0 +1,141 @@ +#!/bin/bash +# Build Android app locally using EAS in WSL2 +# Usage: ./scripts/build-android-local.sh [profile] +# Profiles: development, preview, production (default: production) + +set -e + +PROFILE="${1:-production}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +MOBILE_DIR="$PROJECT_ROOT/apps/mobile" + +echo "==========================================" +echo " Sauci Android Local Build (WSL2)" +echo " Profile: $PROFILE" +echo "==========================================" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +check_dependency() { + if ! command -v "$1" &> /dev/null; then + echo -e "${RED}✗ $1 is not installed${NC}" + return 1 + else + echo -e "${GREEN}✓ $1 found${NC}" + return 0 + fi +} + +# Check dependencies +echo "" +echo "Checking dependencies..." +DEPS_OK=true + +check_dependency "node" || DEPS_OK=false +check_dependency "npm" || DEPS_OK=false +check_dependency "java" || DEPS_OK=false + +# Check Java version (need JDK 17) +if command -v java &> /dev/null; then + JAVA_VERSION=$(java -version 2>&1 | head -1 | cut -d'"' -f2 | cut -d'.' -f1) + if [ "$JAVA_VERSION" -lt 17 ] 2>/dev/null; then + echo -e "${YELLOW}⚠ Java $JAVA_VERSION found, but JDK 17+ recommended${NC}" + fi +fi + +# Check/set ANDROID_HOME +if [ -z "$ANDROID_HOME" ]; then + # Try common WSL2 paths to Windows Android SDK + POSSIBLE_PATHS=( + "/mnt/c/Users/$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r')/AppData/Local/Android/Sdk" + "$HOME/Android/Sdk" + "/usr/lib/android-sdk" + ) + + for SDK_PATH in "${POSSIBLE_PATHS[@]}"; do + if [ -d "$SDK_PATH" ]; then + export ANDROID_HOME="$SDK_PATH" + echo -e "${GREEN}✓ Found Android SDK at $ANDROID_HOME${NC}" + break + fi + done + + if [ -z "$ANDROID_HOME" ]; then + echo -e "${RED}✗ ANDROID_HOME not set and SDK not found${NC}" + echo " Install Android SDK or set ANDROID_HOME manually" + DEPS_OK=false + fi +else + echo -e "${GREEN}✓ ANDROID_HOME=$ANDROID_HOME${NC}" +fi + +# Set ANDROID_SDK_ROOT (some tools use this) +export ANDROID_SDK_ROOT="$ANDROID_HOME" + +# Add platform-tools to PATH +if [ -n "$ANDROID_HOME" ]; then + export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools:$PATH" +fi + +if [ "$DEPS_OK" = false ]; then + echo "" + echo -e "${RED}Missing dependencies. Please install them first:${NC}" + echo "" + echo " # Install Node.js (via nvm)" + echo " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash" + echo " nvm install 20" + echo "" + echo " # Install JDK 17" + echo " sudo apt update && sudo apt install openjdk-17-jdk" + echo "" + echo " # Install Android SDK (or use Windows SDK via /mnt/c/...)" + echo " sudo apt install android-sdk" + echo "" + exit 1 +fi + +# Navigate to mobile app +cd "$MOBILE_DIR" +echo "" +echo "Working directory: $(pwd)" + +# Install dependencies if needed +if [ ! -d "node_modules" ]; then + echo "" + echo "Installing dependencies..." + npm ci +fi + +# Check if eas-cli is available +if ! command -v eas &> /dev/null; then + echo "" + echo "Installing EAS CLI..." + npm install -g eas-cli +fi + +# Create output directory +OUTPUT_DIR="$MOBILE_DIR/build-output" +mkdir -p "$OUTPUT_DIR" + +# Run the build +echo "" +echo "Starting EAS local build..." +echo "This may take 10-20 minutes..." +echo "" + +eas build \ + --local \ + --platform android \ + --profile "$PROFILE" \ + --output "$OUTPUT_DIR/sauci-$PROFILE.aab" + +echo "" +echo "==========================================" +echo -e "${GREEN}Build complete!${NC}" +echo "Output: $OUTPUT_DIR/sauci-$PROFILE.aab" +echo "==========================================" diff --git a/scripts/setup-wsl-android.sh b/scripts/setup-wsl-android.sh new file mode 100644 index 0000000..8f24f38 --- /dev/null +++ b/scripts/setup-wsl-android.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# Setup WSL2 environment for Android builds +# Run this once to install all dependencies + +set -e + +echo "==========================================" +echo " WSL2 Android Build Environment Setup" +echo "==========================================" + +# Update system +echo "" +echo "Updating system packages..." +sudo apt-get update + +# Install JDK 17 (try multiple approaches) +echo "" +echo "Installing OpenJDK 17..." +if ! sudo apt-get install -y openjdk-17-jdk 2>/dev/null; then + echo "Adding Ubuntu toolchain PPA for JDK 17..." + sudo apt-get install -y software-properties-common + sudo add-apt-repository -y ppa:openjdk-r/ppa + sudo apt-get update + sudo apt-get install -y openjdk-17-jdk +fi + +# Set JAVA_HOME +echo "" +echo "Configuring JAVA_HOME..." +JAVA_PATH=$(dirname $(dirname $(readlink -f $(which java)))) +if ! grep -q "JAVA_HOME" ~/.bashrc; then + echo "export JAVA_HOME=$JAVA_PATH" >> ~/.bashrc +fi +export JAVA_HOME=$JAVA_PATH +echo "JAVA_HOME=$JAVA_HOME" + +# Install build essentials +echo "" +echo "Installing build tools..." +sudo apt-get install -y build-essential curl git unzip + +# Install Node.js via nvm +echo "" +if command -v node &> /dev/null; then + echo "Node.js already installed: $(node -v)" +else + echo "Installing Node.js via nvm..." + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash + + # Load nvm for this session + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + + nvm install 20 + nvm use 20 +fi + +# Install EAS CLI +echo "" +echo "Installing EAS CLI..." +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" +npm install -g eas-cli + +# Configure Android SDK path (use Windows SDK) +echo "" +echo "Configuring Android SDK..." +WIN_USER=$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r\n') +WIN_SDK_PATH="/mnt/c/Users/$WIN_USER/AppData/Local/Android/Sdk" + +if [ -d "$WIN_SDK_PATH" ]; then + echo "Found Windows Android SDK at: $WIN_SDK_PATH" + if ! grep -q "ANDROID_HOME" ~/.bashrc; then + echo "export ANDROID_HOME=\"$WIN_SDK_PATH\"" >> ~/.bashrc + echo "export ANDROID_SDK_ROOT=\"$WIN_SDK_PATH\"" >> ~/.bashrc + echo 'export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"' >> ~/.bashrc + fi +else + echo "Windows Android SDK not found at: $WIN_SDK_PATH" + echo "You may need to set ANDROID_HOME manually in ~/.bashrc" +fi + +echo "" +echo "==========================================" +echo " Setup complete!" +echo "==========================================" +echo "" +echo "Run this to reload your shell:" +echo " source ~/.bashrc" +echo "" +echo "Then build with:" +echo " cd /mnt/i/Projects/Sauci" +echo " ./scripts/build-android-local.sh" +echo ""