From 4d29a097a373efa1fc90660c7dfc07a780f96d90 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Wed, 19 Nov 2025 09:42:02 -0800 Subject: [PATCH] Update mobile app dependencies and screens --- backend/docker-compose.gemini.yml | 121 +++++++++++++++ backend/nginx.gemini.conf | 100 ++++++++++++ backend/requirements.txt | 6 +- mobile/app/(tabs)/_layout.tsx | 11 ++ mobile/app/(tabs)/community.tsx | 129 ++++++++-------- mobile/app/(tabs)/podcasts.tsx | 242 ++++++++++++++++++++++++++++++ mobile/app/(tabs)/resources.tsx | 82 +++++----- mobile/constants/Colors.ts | 12 ++ mobile/constants/Theme.ts | 48 ++++++ mobile/package-lock.json | 10 ++ mobile/package.json | 1 + 11 files changed, 662 insertions(+), 100 deletions(-) create mode 100644 backend/docker-compose.gemini.yml create mode 100644 backend/nginx.gemini.conf create mode 100644 mobile/app/(tabs)/podcasts.tsx create mode 100644 mobile/constants/Colors.ts create mode 100644 mobile/constants/Theme.ts diff --git a/backend/docker-compose.gemini.yml b/backend/docker-compose.gemini.yml new file mode 100644 index 0000000..7583ff1 --- /dev/null +++ b/backend/docker-compose.gemini.yml @@ -0,0 +1,121 @@ + version: '3.8' + +# Gemini Parallel Deployment +# Ports shifted to avoid conflict with production + +services: + postgres: + image: postgres:15-alpine + container_name: mtad-postgres-gemini + restart: unless-stopped + environment: + POSTGRES_USER: admin + POSTGRES_PASSWORD: ${DB_PASSWORD:-gemini-test-password} + POSTGRES_DB: morethanadiagnosis + POSTGRES_INITDB_ARGS: "-c shared_preload_libraries=pg_stat_statements" + volumes: + - postgres_data_gemini:/var/lib/postgresql/data + ports: + - "5433:5432" + networks: + - mtad-network-gemini + healthcheck: + test: ["CMD-SHELL", "pg_isready -U admin -d morethanadiagnosis"] + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + container_name: mtad-redis-gemini + restart: unless-stopped + ports: + - "6380:6379" + volumes: + - redis_data_gemini:/data + networks: + - mtad-network-gemini + command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-gemini-test-password} + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + api: + build: + context: . + dockerfile: Dockerfile + container_name: mtad-api-gemini + restart: unless-stopped + environment: + ENV: development + DEBUG: "true" + DATABASE_URL: postgresql://admin:${DB_PASSWORD:-gemini-test-password}@postgres:5432/morethanadiagnosis + REDIS_URL: redis://:${REDIS_PASSWORD:-gemini-test-password}@redis:6379/0 + SECRET_KEY: ${SECRET_KEY:-gemini-test-secret} + CORS_ORIGINS: '["http://localhost:3001", "http://localhost:8081"]' + LOG_LEVEL: DEBUG + ports: + - "8001:8000" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - mtad-network-gemini + volumes: + - ./app:/app/app:ro + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + + frontend: + build: + context: ../web + dockerfile: Dockerfile + container_name: mtad-web-gemini + restart: unless-stopped + expose: + - "3000" + networks: + - mtad-network-gemini + depends_on: + - api + environment: + NEXT_PUBLIC_API_BASE_URL: http://api:8000/api/v1 + + nginx: + image: nginx:alpine + container_name: mtad-nginx-gemini + restart: unless-stopped + ports: + - "8081:80" + # - "8443:443" # SSL disabled for gemini test to avoid cert conflicts + volumes: + - ./nginx.gemini.conf:/etc/nginx/nginx.conf:ro + # - ./certbot/conf:/etc/letsencrypt:ro # Disable SSL for test + - ../web/out:/usr/share/nginx/html:ro + networks: + - mtad-network-gemini + depends_on: + - api + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + +networks: + mtad-network-gemini: + driver: bridge + +volumes: + postgres_data_gemini: + driver: local + redis_data_gemini: + driver: local diff --git a/backend/nginx.gemini.conf b/backend/nginx.gemini.conf new file mode 100644 index 0000000..4ef213e --- /dev/null +++ b/backend/nginx.gemini.conf @@ -0,0 +1,100 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + use epoll; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 100M; + + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml text/javascript + application/json application/javascript application/xml+rss + application/rss+xml font/truetype font/opentype + application/vnd.ms-fontobject image/svg+xml; + + # Upstream API + upstream api { + least_conn; + server api:8000 max_fails=3 fail_timeout=30s; + keepalive 32; + } + + # HTTP server - Gemini Test Deployment + server { + listen 80; + server_name _; # Catch all + + # Proxy frontend requests to Next.js service + location / { + proxy_pass http://frontend:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # API endpoints + location /api/v1/ { + proxy_pass http://api; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Connection ""; + proxy_redirect off; + } + + location /docs { + proxy_pass http://api; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /redoc { + proxy_pass http://api; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /openapi.json { + proxy_pass http://api; + proxy_set_header Host $host; + } + + location /health { + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + } +} diff --git a/backend/requirements.txt b/backend/requirements.txt index 933620b..4143952 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -9,12 +9,12 @@ python-multipart==0.0.6 sqlalchemy==2.0.23 alembic==1.12.1 psycopg2-binary==2.9.9 -psycopg[binary]==3.9.10 +psycopg[binary]==3.1.18 # Authentication & Security passlib[bcrypt]==1.7.4 python-jose[cryptography]==3.3.0 -PyJWT==2.8.1 +PyJWT>=2.8.0 bcrypt==4.1.1 cryptography==41.0.7 argon2-cffi==23.1.0 @@ -48,7 +48,7 @@ httpx==0.25.2 # Linting & Type Checking ruff==0.1.8 mypy==1.7.1 -types-python-dateutil==2.8.20 +types-python-dateutil>=2.8.19 # Development black==23.12.0 diff --git a/mobile/app/(tabs)/_layout.tsx b/mobile/app/(tabs)/_layout.tsx index 2064c81..f086cec 100644 --- a/mobile/app/(tabs)/_layout.tsx +++ b/mobile/app/(tabs)/_layout.tsx @@ -54,6 +54,17 @@ export default function TabLayout() { }} /> + ( + + ), + }} + /> + + + } > @@ -189,12 +198,14 @@ export default function CommunityScreen() { - {community.icon} + {community.icon} + + + {community.title} + {community.members} - {community.title} {community.description} - {community.members} Join Community @@ -229,9 +240,9 @@ export default function CommunityScreen() { )} - - Community Guidelines - + + Community Guidelines + • Be respectful and supportive{'\n'} • Share your authentic story{'\n'} • Respect others' privacy{'\n'} diff --git a/mobile/app/(tabs)/podcasts.tsx b/mobile/app/(tabs)/podcasts.tsx new file mode 100644 index 0000000..cd6ff1f --- /dev/null +++ b/mobile/app/(tabs)/podcasts.tsx @@ -0,0 +1,242 @@ +import React, { useState, useEffect } from 'react'; +import { View, Text, FlatList, StyleSheet, TouchableOpacity, Image, ActivityIndicator } from 'react-native'; +import { useQuery } from '@tanstack/react-query'; +import { Audio } from 'expo-av'; +import { Theme } from '../../constants/Theme'; +import { Ionicons } from '@expo/vector-icons'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +const API_URL = process.env.EXPO_PUBLIC_API_URL || 'http://216.158.230.94:8001/api/v1'; + +interface PodcastEpisode { + id: string; + title: string; + description: string; + audio_url: string; + published_at: string; + duration: string; + image_url: string; +} + +interface PodcastFeed { + title: string; + description: string; + image_url: string; + episodes: PodcastEpisode[]; +} + +const fetchPodcasts = async (): Promise => { + const response = await fetch(`${API_URL}/podcast/`); + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); +}; + +export default function PodcastsScreen() { + const { data, isLoading, error } = useQuery({ + queryKey: ['podcasts'], + queryFn: fetchPodcasts, + }); + + const [sound, setSound] = useState(null); + const [isPlaying, setIsPlaying] = useState(false); + const [currentEpisodeId, setCurrentEpisodeId] = useState(null); + + useEffect(() => { + return () => { + if (sound) { + sound.unloadAsync(); + } + }; + }, [sound]); + + const playPodcast = async (audioUrl: string, episodeId: string) => { + try { + if (sound) { + await sound.unloadAsync(); + } + + const { sound: newSound } = await Audio.Sound.createAsync( + { uri: audioUrl }, + { shouldPlay: true } + ); + + setSound(newSound); + setCurrentEpisodeId(episodeId); + setIsPlaying(true); + + newSound.setOnPlaybackStatusUpdate((status) => { + if (status.isLoaded) { + setIsPlaying(status.isPlaying); + if (status.didJustFinish) { + setIsPlaying(false); + setCurrentEpisodeId(null); + } + } + }); + } catch (error) { + console.error('Error playing sound:', error); + } + }; + + const pausePodcast = async () => { + if (sound) { + await sound.pauseAsync(); + setIsPlaying(false); + } + }; + + const resumePodcast = async () => { + if (sound) { + await sound.playAsync(); + setIsPlaying(true); + } + }; + + const handlePlayPause = (audioUrl: string, episodeId: string) => { + if (currentEpisodeId === episodeId) { + if (isPlaying) { + pausePodcast(); + } else { + resumePodcast(); + } + } else { + playPodcast(audioUrl, episodeId); + } + }; + + const renderItem = ({ item }: { item: PodcastEpisode }) => ( + + + + + {item.title} + {new Date(item.published_at).toLocaleDateString()} + {item.duration} + + handlePlayPause(item.audio_url, item.id)} + > + + + + + ); + + if (isLoading) { + return ( + + + + ); + } + + if (error) { + return ( + + Failed to load podcasts + + ); + } + + return ( + + item.id} + contentContainerStyle={styles.listContent} + ListHeaderComponent={ + + Podcasts + Listen to the latest episodes + + } + /> + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: Theme.colors.background, + }, + center: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + listContent: { + padding: Theme.spacing.m, + }, + header: { + marginBottom: Theme.spacing.l, + }, + headerTitle: { + fontSize: Theme.typography.fontSize.xxl, + fontFamily: Theme.typography.fontFamily.heading, + color: Theme.colors.text, + fontWeight: 'bold', + }, + headerSubtitle: { + fontSize: Theme.typography.fontSize.m, + fontFamily: Theme.typography.fontFamily.body, + color: Theme.colors.text, + opacity: 0.7, + }, + card: { + backgroundColor: Theme.colors.surface, + borderRadius: Theme.borderRadius.m, + padding: Theme.spacing.m, + marginBottom: Theme.spacing.m, + ...Theme.shadows.card, + }, + cardContent: { + flexDirection: 'row', + alignItems: 'center', + }, + thumbnail: { + width: 60, + height: 60, + borderRadius: Theme.borderRadius.s, + marginRight: Theme.spacing.m, + backgroundColor: Theme.colors.secondary, + }, + textContainer: { + flex: 1, + marginRight: Theme.spacing.s, + }, + episodeTitle: { + fontSize: Theme.typography.fontSize.m, + fontFamily: Theme.typography.fontFamily.heading, + color: Theme.colors.text, + fontWeight: '600', + marginBottom: 4, + }, + episodeDate: { + fontSize: Theme.typography.fontSize.xs, + fontFamily: Theme.typography.fontFamily.body, + color: Theme.colors.text, + opacity: 0.6, + }, + episodeDuration: { + fontSize: Theme.typography.fontSize.xs, + fontFamily: Theme.typography.fontFamily.body, + color: Theme.colors.text, + opacity: 0.6, + }, + playButton: { + padding: 4, + }, + errorText: { + color: Theme.colors.error || 'red', + fontSize: Theme.typography.fontSize.m, + }, +}); diff --git a/mobile/app/(tabs)/resources.tsx b/mobile/app/(tabs)/resources.tsx index 0168e5d..37754c9 100644 --- a/mobile/app/(tabs)/resources.tsx +++ b/mobile/app/(tabs)/resources.tsx @@ -7,75 +7,75 @@ import { SafeAreaView, TouchableOpacity, RefreshControl, + StatusBar, } from 'react-native'; import { useResources } from '../../hooks/useResources'; import { LoadingState } from '../../components/ui/LoadingState'; import { ErrorState } from '../../components/ui/ErrorState'; import { EmptyState } from '../../components/ui/EmptyState'; +import { Theme } from '../../constants/Theme'; const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#f5f5f5', + backgroundColor: Theme.colors.background, }, header: { - backgroundColor: '#fff', - padding: 16, + backgroundColor: Theme.colors.surface, + padding: Theme.spacing.l, borderBottomWidth: 1, - borderBottomColor: '#e0e0e0', + borderBottomColor: Theme.colors.border, }, headerTitle: { - fontSize: 24, - fontWeight: '700', - color: '#000', - marginBottom: 8, + ...Theme.typography.heading, + color: Theme.colors.primary, + marginBottom: Theme.spacing.s, }, headerDescription: { - fontSize: 14, - color: '#666', - lineHeight: 20, + ...Theme.typography.body, + color: Theme.colors.textMuted, }, section: { - paddingHorizontal: 16, - marginTop: 16, + paddingHorizontal: Theme.spacing.m, + marginTop: Theme.spacing.l, }, sectionTitle: { - fontSize: 16, - fontWeight: '600', - color: '#000', - marginBottom: 12, + ...Theme.typography.subheading, + color: Theme.colors.text, + marginBottom: Theme.spacing.m, }, resourceCard: { - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - marginBottom: 12, + backgroundColor: Theme.colors.surface, + borderRadius: Theme.borderRadius.m, + padding: Theme.spacing.m, + marginBottom: Theme.spacing.m, borderLeftWidth: 4, - borderLeftColor: '#0066cc', + borderLeftColor: Theme.colors.primary, + ...Theme.shadows.small, }, resourceTitle: { - fontSize: 15, - fontWeight: '600', - color: '#000', - marginBottom: 6, + ...Theme.typography.subheading, + fontSize: 16, + color: Theme.colors.text, + marginBottom: Theme.spacing.xs, }, resourceDescription: { - fontSize: 13, - color: '#666', - lineHeight: 18, + ...Theme.typography.body, + fontSize: 14, + color: Theme.colors.textMuted, + marginBottom: Theme.spacing.m, }, resourceLink: { - marginTop: 10, - paddingVertical: 8, - paddingHorizontal: 12, - backgroundColor: '#f0f0f0', - borderRadius: 6, + paddingVertical: Theme.spacing.s, + paddingHorizontal: Theme.spacing.m, + backgroundColor: Theme.colors.secondaryLight, + borderRadius: Theme.borderRadius.s, + alignSelf: 'flex-start', }, resourceLinkText: { - color: '#0066cc', + color: Theme.colors.primaryDark, fontSize: 12, fontWeight: '600', - textAlign: 'center', }, }); @@ -96,10 +96,16 @@ export default function ResourcesScreen() { return ( + + } > @@ -127,7 +133,7 @@ export default function ResourcesScreen() { More Coming Soon - + Be sure to check back often or sign up to receive updates as we're adding new resources all the time! diff --git a/mobile/constants/Colors.ts b/mobile/constants/Colors.ts new file mode 100644 index 0000000..395e95e --- /dev/null +++ b/mobile/constants/Colors.ts @@ -0,0 +1,12 @@ +export const Colors = { + text: '#FAF9F6', // Cream Text + textMuted: '#9CA3AF', + textLight: '#FFFFFF', + + error: '#EF4444', + success: '#10B981', + warning: '#F59E0B', + + border: '#374151', +}, +}; diff --git a/mobile/constants/Theme.ts b/mobile/constants/Theme.ts new file mode 100644 index 0000000..93b60ce --- /dev/null +++ b/mobile/constants/Theme.ts @@ -0,0 +1,48 @@ +import { Colors } from './Colors'; + +export const Theme = { + colors: Colors.light, // Default to light theme for now + spacing: { + xs: 4, + s: 8, + m: 16, + l: 24, + xl: 32, + xxl: 48, + }, + borderRadius: { + s: 8, + m: 12, + l: 16, + xl: 24, + round: 9999, + }, + typography: { + fontFamily: { + heading: 'System', // Placeholder + body: 'System', // Placeholder + }, + fontSize: { + xs: 12, + s: 14, + m: 16, + l: 20, + xl: 24, + xxl: 32, + }, + fontWeight: { + regular: '400' as const, + medium: '500' as const, + bold: '700' as const, + }, + }, + shadows: { + card: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.05, + shadowRadius: 8, + elevation: 2, + }, + }, +}; diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 00fba70..cdd17ea 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -12,6 +12,7 @@ "@tanstack/react-query": "^5.90.10", "axios": "^1.6.0", "expo": "^51.0.0", + "expo-av": "~14.0.7", "expo-constants": "~15.4.0", "expo-linking": "~6.0.0", "expo-router": "^3.4.0", @@ -8347,6 +8348,15 @@ "expo": "*" } }, + "node_modules/expo-av": { + "version": "14.0.7", + "resolved": "https://registry.npmjs.org/expo-av/-/expo-av-14.0.7.tgz", + "integrity": "sha512-FvKZxyy+2/qcCmp+e1GTK3s4zH8ZO1RfjpqNxh7ARlS1oH8HPtk1AyZAMo52tHz3yQ3UIqxQ2YbI9CFb4065lA==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-constants": { "version": "15.4.6", "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-15.4.6.tgz", diff --git a/mobile/package.json b/mobile/package.json index eaeee2a..e926c31 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -20,6 +20,7 @@ "@tanstack/react-query": "^5.90.10", "axios": "^1.6.0", "expo": "^51.0.0", + "expo-av": "~14.0.7", "expo-constants": "~15.4.0", "expo-linking": "~6.0.0", "expo-router": "^3.4.0",