From df801dee3225ee8885a8c58400f15ef59f136c35 Mon Sep 17 00:00:00 2001 From: nictjh Date: Tue, 14 Oct 2025 13:22:54 +0800 Subject: [PATCH 1/4] Add theme into home and other screens UI --- android/app/src/main/AndroidManifest.xml | 4 +- app/LiveChatScreen.jsx | 377 ++++++++++++++++++----- app/_layout.jsx | 9 +- app/home.jsx | 328 ++++++++++++-------- app/userinfo.jsx | 371 ++++++++++++++-------- package-lock.json | 2 +- 6 files changed, 769 insertions(+), 322 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d1df0cd..37dfb5d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -15,8 +15,8 @@ - - + + diff --git a/app/LiveChatScreen.jsx b/app/LiveChatScreen.jsx index c7965cf..1086797 100644 --- a/app/LiveChatScreen.jsx +++ b/app/LiveChatScreen.jsx @@ -9,7 +9,9 @@ import { KeyboardAvoidingView, Platform, StyleSheet, + StatusBar, } from 'react-native'; +import { LinearGradient } from 'expo-linear-gradient'; import { useLocalSearchParams, useRouter } from 'expo-router'; @@ -128,99 +130,334 @@ export default function LiveChatScreen() { const renderItem = ({ item }) => { const mine = item.sender_role === 'customer'; return ( - - {item.content} + + + + {item.content} + + + {new Date(item.ts || Date.now()).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + + ); }; return ( - - - - Live Support - - you: {String(customerId)} - - conn: {connected ? 'online' : 'offline'} - - {queuePos != null && ( - queue: #{queuePos} - )} + + + + + + + + + 💬 + + Live Support + + router.back()}> + Back + + + + + + + {connected ? 'Connected' : 'Connecting...'} + + + {conversationId && ( + + Chat ID: {conversationId} + + )} + {queuePos != null && ( + + Queue position: #{queuePos} + + )} + - - item.id || String(idx)} - renderItem={renderItem} - contentContainerStyle={styles.listContent} - /> - - - + item.id || String(idx)} + renderItem={renderItem} + contentContainerStyle={styles.listContent} + showsVerticalScrollIndicator={false} /> - - Send - - - End + + + + + + + Send + + + + End Chat - - + + + ); } const styles = StyleSheet.create({ - container: { flex: 1, backgroundColor: '#0b0b0f' }, - header: { paddingHorizontal: 16, paddingTop: 8, paddingBottom: 6, backgroundColor: '#111318' }, - title: { color: 'white', fontSize: 18, fontWeight: '600' }, - metaRow: { flexDirection: 'row', marginTop: 4 }, - meta: { color: '#9aa0a6', fontSize: 12 }, - listContent: { padding: 12 }, - bubble: { - maxWidth: '80%', - paddingHorizontal: 12, + container: { + flex: 1, + }, + header: { + paddingHorizontal: 24, + paddingTop: 16, + paddingBottom: 20, + }, + headerTop: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 16, + }, + logoContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + logo: { + width: 48, + height: 48, + borderRadius: 24, + backgroundColor: 'rgba(220, 178, 78, 0.15)', + justifyContent: 'center', + alignItems: 'center', + marginRight: 16, + borderWidth: 2, + borderColor: '#dcb24e', + }, + logoText: { + fontSize: 24, + }, + title: { + color: '#fffffe', + fontSize: 28, + fontWeight: '700', + letterSpacing: 0.5, + }, + backButton: { paddingVertical: 8, + paddingHorizontal: 16, + backgroundColor: 'rgba(220, 178, 78, 0.15)', + borderRadius: 20, + borderWidth: 1, + borderColor: '#dcb24e', + }, + backButtonText: { + fontSize: 14, + color: '#dcb24e', + fontWeight: '600', + }, + statusContainer: { + flexDirection: 'row', + alignItems: 'center', + flexWrap: 'wrap', + gap: 8, + }, + statusBadge: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: 'rgba(255, 255, 255, 0.1)', + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 16, + }, + statusDot: { + width: 8, + height: 8, + borderRadius: 4, + marginRight: 8, + }, + statusText: { + color: '#fffffe', + fontSize: 14, + fontWeight: '500', + }, + conversationBadge: { + backgroundColor: 'rgba(220, 178, 78, 0.15)', + paddingHorizontal: 12, + paddingVertical: 6, borderRadius: 16, - marginVertical: 6, + borderWidth: 1, + borderColor: 'rgba(220, 178, 78, 0.4)', + }, + conversationText: { + color: '#dcb24e', + fontSize: 12, + fontWeight: '600', + letterSpacing: 0.5, + }, + queueBadge: { + backgroundColor: 'rgba(220, 178, 78, 0.2)', + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 16, + borderWidth: 1, + borderColor: '#dcb24e', + }, + queueText: { + color: '#dcb24e', + fontSize: 14, + fontWeight: '600', + }, + chatContainer: { + flex: 1, + backgroundColor: 'rgba(255, 255, 255, 0.05)', + marginHorizontal: 16, + marginVertical: 8, + borderRadius: 20, + borderWidth: 1, + borderColor: 'rgba(255, 255, 255, 0.1)', + }, + listContent: { + padding: 20, + paddingBottom: 24, + }, + messageContainer: { + marginVertical: 4, + }, + myMessage: { + alignItems: 'flex-end', + }, + theirMessage: { + alignItems: 'flex-start', + }, + bubble: { + maxWidth: '75%', + paddingHorizontal: 16, + paddingVertical: 12, + borderRadius: 20, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + myBubble: { + backgroundColor: '#dcb24e', + borderBottomRightRadius: 6, + }, + theirBubble: { + backgroundColor: 'rgba(255, 255, 255, 0.95)', + borderBottomLeftRadius: 6, + borderWidth: 1, + borderColor: 'rgba(220, 178, 78, 0.2)', + }, + bubbleText: { + fontSize: 16, + lineHeight: 22, + fontWeight: '400', + }, + myText: { + color: '#0e273c', + }, + theirText: { + color: '#0e273c', + }, + timestamp: { + fontSize: 11, + marginTop: 4, + fontWeight: '500', + }, + myTimestamp: { + color: 'rgba(14, 39, 60, 0.7)', + textAlign: 'right', + }, + theirTimestamp: { + color: '#9ca3af', + textAlign: 'left', + }, + inputContainer: { + backgroundColor: 'rgba(255, 255, 255, 0.95)', + paddingHorizontal: 20, + paddingVertical: 16, + borderTopLeftRadius: 24, + borderTopRightRadius: 24, + borderWidth: 1, + borderColor: 'rgba(220, 178, 78, 0.2)', + shadowColor: '#0e273c', + shadowOffset: { + width: 0, + height: -4, + }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 8, }, - mine: { alignSelf: 'flex-end', backgroundColor: '#1e88e5' }, - theirs: { alignSelf: 'flex-start', backgroundColor: '#2f333a' }, - bubbleText: { color: 'white', fontSize: 16 }, inputRow: { flexDirection: 'row', - alignItems: 'center', - padding: 10, - backgroundColor: '#111318', + alignItems: 'flex-end', + marginBottom: 12, }, input: { flex: 1, - backgroundColor: '#1a1d24', - color: 'white', - paddingHorizontal: 12, - paddingVertical: 10, - borderRadius: 12, - marginRight: 8, + backgroundColor: '#f8f9fa', + color: '#0e273c', + paddingHorizontal: 16, + paddingVertical: 12, + borderRadius: 20, + marginRight: 12, + fontSize: 16, + maxHeight: 100, + borderWidth: 1, + borderColor: 'rgba(220, 178, 78, 0.3)', + }, + sendBtn: { + backgroundColor: '#dcb24e', + paddingHorizontal: 20, + paddingVertical: 12, + borderRadius: 20, + shadowColor: '#dcb24e', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 4, }, - btn: { - paddingHorizontal: 14, + sendBtnText: { + color: '#0e273c', + fontWeight: '700', + fontSize: 16, + }, + endBtn: { + backgroundColor: 'rgba(239, 68, 68, 0.1)', paddingVertical: 10, - backgroundColor: '#3949ab', - borderRadius: 12, - marginLeft: 6, + paddingHorizontal: 20, + borderRadius: 16, + alignItems: 'center', + borderWidth: 1, + borderColor: '#ef4444', + }, + endBtnText: { + color: '#ef4444', + fontWeight: '600', + fontSize: 14, }, - endBtn: { backgroundColor: '#b00020' }, - btnText: { color: 'white', fontWeight: '600' }, }); diff --git a/app/_layout.jsx b/app/_layout.jsx index f07959a..326e1dd 100644 --- a/app/_layout.jsx +++ b/app/_layout.jsx @@ -17,7 +17,7 @@ export default function RootLayout() { const checkResult = await PermissionsAndroid.check( PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS ); - + if (checkResult) { console.log('Notification permission already granted'); return true; @@ -164,6 +164,13 @@ export default function RootLayout() { }} /> + + { + const handleChat = () => { // Changed this to LiveChat for now router.push({ pathname: '/LiveChatScreen', @@ -227,17 +228,13 @@ export default function Home() { const handleUpdateParticulars = () => { // Navigate to user info page - router.push('/userinfo'); - }; - - const handleChangeUserPin = () => { - // TODO: Add Change User ID/Pin functionality - console.log('Change User ID/Pin pressed'); - }; - - const handleCloseProfile = () => { - // TODO: Add Close Bank Profile functionality - console.log('Close Bank Profile pressed'); + // router.push('/userinfo'); + router.push({ + pathname: '/userinfo', + params: { + customerId: customerId + } + }); }; function handleOpenWebview() { @@ -249,22 +246,35 @@ export default function Home() { return ( - - + - - - Logout - - - - - Welcome Back - {user.email} - + + + + + + 💳 + + Zentra Bank + + + Logout + + + + Welcome Back + {user.email} + + {/* Account Balance Card */} @@ -305,68 +315,49 @@ export default function Home() { 💳 - PayNow + PayNow 📱 - Scan & Pay + Scan & Pay 💸 - Fund Transfer + Fund Transfer - {/* Apply Card */} + {/* Others Card */} - Apply + Others 🏦 - Accounts + Accounts - - - 💳 - - Cards - - - - {/* Profile Card */} - - Profile - ✏️ - Update Particulars - - - - - 🔐 - - Change User ID/Pin + View Profile - + - + 💬 - Close Bank Profile + Support @@ -384,7 +375,8 @@ export default function Home() { - + + ); } @@ -392,126 +384,184 @@ export default function Home() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#f8f9fa', + backgroundColor: '#0e273c', + }, + gradient: { + flex: 1, }, scrollView: { flex: 1, }, scrollContent: { - paddingBottom: 24, // Add bottom padding for better scrolling + paddingBottom: 24, }, - headerLogOut: { - flexDirection: 'row', - justifyContent: 'flex-end', + headerContainer: { paddingHorizontal: 24, - paddingTop: 24, + paddingTop: 20, + paddingBottom: 20, + }, + headerTop: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 20, + }, + logoContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + logo: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: 'rgba(220, 178, 78, 0.2)', + justifyContent: 'center', + alignItems: 'center', + marginRight: 12, + borderWidth: 1, + borderColor: '#dcb24e', + }, + logoText: { + fontSize: 20, + }, + bankName: { + fontSize: 20, + fontWeight: '700', + color: '#fffffe', + letterSpacing: 1, }, header: { - flexDirection: 'column', alignItems: 'flex-start', - paddingHorizontal: 24, - paddingTop: 24, }, welcomeText: { - fontSize: 24, - fontWeight: '600', - color: '#1f2937', + fontSize: 28, + fontWeight: '700', + color: '#fffffe', + letterSpacing: 0.5, }, userEmail: { - fontSize: 14, - color: '#6b7280', + fontSize: 16, + color: '#dcb24e', marginTop: 4, + fontWeight: '400', }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', + backgroundColor: '#0e273c', }, loadingText: { - fontSize: 16, - color: '#6b7280', + fontSize: 18, + color: '#fffffe', + fontWeight: '500', }, logoutButton: { - padding: 8, + paddingVertical: 8, + paddingHorizontal: 16, + backgroundColor: 'rgba(220, 178, 78, 0.15)', + borderRadius: 20, + borderWidth: 1, + borderColor: '#dcb24e', }, logoutText: { - fontSize: 16, - color: '#ef4444', - fontWeight: '500', + fontSize: 14, + color: '#dcb24e', + fontWeight: '600', }, balanceCard: { - backgroundColor: '#ffffff', + backgroundColor: '#fffffe', marginHorizontal: 24, marginTop: 20, - padding: 24, - borderRadius: 16, + padding: 28, + borderRadius: 20, shadowColor: '#000', shadowOffset: { width: 0, - height: 2, + height: 8, }, - shadowOpacity: 0.1, - shadowRadius: 8, - elevation: 4, + shadowOpacity: 0.15, + shadowRadius: 16, + elevation: 8, + borderWidth: 1, + borderColor: 'rgba(220, 178, 78, 0.1)', }, balanceLabel: { - fontSize: 14, + fontSize: 15, color: '#6b7280', - marginBottom: 8, + marginBottom: 12, + fontWeight: '500', }, balanceAmount: { - fontSize: 32, - fontWeight: 'bold', - color: '#1f2937', + fontSize: 36, + fontWeight: '700', + color: '#0e273c', marginBottom: 8, + letterSpacing: -0.5, }, accountNumber: { - fontSize: 14, + fontSize: 15, color: '#9ca3af', + fontWeight: '500', }, noAccountMessage: { - fontSize: 18, - fontWeight: '600', - color: '#374151', - marginBottom: 4, + fontSize: 20, + fontWeight: '700', + color: '#0e273c', + marginBottom: 6, + textAlign: 'center', }, noAccountSubtext: { - fontSize: 14, + fontSize: 15, color: '#6b7280', - marginBottom: 16, + marginBottom: 20, + textAlign: 'center', + lineHeight: 22, }, createAccountButton: { - backgroundColor: '#3b82f6', - paddingVertical: 12, - paddingHorizontal: 24, - borderRadius: 8, + backgroundColor: '#dcb24e', + paddingVertical: 14, + paddingHorizontal: 28, + borderRadius: 12, alignItems: 'center', + shadowColor: '#dcb24e', + shadowOffset: { + width: 0, + height: 4, + }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 6, }, createAccountButtonText: { - color: '#ffffff', - fontSize: 14, + color: '#fffffe', + fontSize: 16, fontWeight: '600', + letterSpacing: 0.5, }, actionsCard: { - backgroundColor: '#ffffff', + backgroundColor: '#fffffe', marginHorizontal: 24, marginTop: 20, - padding: 24, - borderRadius: 16, + padding: 28, + borderRadius: 20, shadowColor: '#000', shadowOffset: { width: 0, - height: 2, + height: 8, }, - shadowOpacity: 0.1, - shadowRadius: 8, - elevation: 4, + shadowOpacity: 0.15, + shadowRadius: 16, + elevation: 8, + borderWidth: 1, + borderColor: 'rgba(220, 178, 78, 0.1)', }, actionsTitle: { - fontSize: 18, - fontWeight: '600', - color: '#1f2937', - marginBottom: 20, + fontSize: 20, + fontWeight: '700', + color: '#0e273c', + marginBottom: 24, + letterSpacing: 0.5, }, actionsContainer: { flexDirection: 'row', @@ -524,32 +574,58 @@ const styles = StyleSheet.create({ actionButton: { alignItems: 'center', flex: 1, - paddingVertical: 16, + paddingVertical: 20, paddingHorizontal: 8, + backgroundColor: 'rgba(220, 178, 78, 0.05)', + borderRadius: 16, + marginHorizontal: 4, + borderWidth: 1, + borderColor: 'rgba(220, 178, 78, 0.1)', + minHeight: 120, }, applyButton: { alignItems: 'center', flex: 1, - paddingVertical: 16, - paddingHorizontal: 16, + paddingVertical: 20, + paddingHorizontal: 12, maxWidth: '45%', + backgroundColor: 'rgba(220, 178, 78, 0.05)', + borderRadius: 16, + marginHorizontal: 8, + borderWidth: 1, + borderColor: 'rgba(220, 178, 78, 0.1)', + minHeight: 120, }, - actionIconPlaceholder: { // Makes it consistent across buttons - width: 48, - height: 48, - borderRadius: 24, - backgroundColor: '#f3f4f6', + actionIconPlaceholder: { + width: 56, + height: 56, + borderRadius: 28, + backgroundColor: 'rgba(220, 178, 78, 0.15)', justifyContent: 'center', alignItems: 'center', - marginBottom: 8, + marginBottom: 12, + borderWidth: 2, + borderColor: 'rgba(220, 178, 78, 0.3)', + shadowColor: '#dcb24e', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, }, actionIconText: { - fontSize: 20, + fontSize: 24, }, actionText: { - fontSize: 14, - fontWeight: '500', - color: '#374151', + fontSize: 12, + fontWeight: '600', + color: '#0e273c', textAlign: 'center', + letterSpacing: 0.3, + lineHeight: 16, + maxWidth: '100%', + flexWrap: 'wrap', }, }); diff --git a/app/userinfo.jsx b/app/userinfo.jsx index 69f5325..4a9c0ec 100644 --- a/app/userinfo.jsx +++ b/app/userinfo.jsx @@ -1,19 +1,19 @@ -import { View, Text, StyleSheet, SafeAreaView, StatusBar, ScrollView, ActivityIndicator } from 'react-native'; -import { useRouter } from 'expo-router'; +import { View, Text, StyleSheet, SafeAreaView, StatusBar, ScrollView, ActivityIndicator, TouchableOpacity } from 'react-native'; +import { LinearGradient } from 'expo-linear-gradient'; +import { useLocalSearchParams, useRouter } from 'expo-router'; import { useEffect, useState } from 'react'; import { supabase } from '../lib/supabase'; import { getUserProfile } from '../lib/services/userService'; export default function UserInfo() { const router = useRouter(); + const { customerId } = useLocalSearchParams(); const [user, setUser] = useState(null); - const [customerData, setCustomerData] = useState(null); - const [customerId, setCustomerId] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { - const fetchUserAndCustomerData = async () => { + const fetchUserData = async () => { try { // Get current session const { data: { session }, error: sessionError } = await supabase.auth.getSession(); @@ -24,127 +24,128 @@ export default function UserInfo() { } setUser(session.user); - - // Use your userService with user.id directly - const customerData = await getUserProfile(session.user.id); - - if (customerData) { - setCustomerData(customerData); - setCustomerId(customerData.customer_id); // Extract customer_id from the data - } else { - setError('Could not find customer profile'); - } - setLoading(false); } catch (err) { - console.error('Error in fetchUserAndCustomerData:', err); + console.error('Error in fetchUserData:', err); setError('An error occurred while loading user data'); setLoading(false); } }; - fetchUserAndCustomerData(); + fetchUserData(); }, []); + const handleContactSupport = () => { + router.push({ + pathname: '/LiveChatScreen', + params: { + customerId: customerId + } + }); + }; + if (loading) { return ( - - - - - Loading user information... - - + + + + + + Loading user information... + + + ); } if (error) { return ( - - - - {error} - - + + + + + {error} + + + ); } return ( - - - + + + + - {/* Header */} - - User Information - - - {/* Auth User Info Card */} - - Authentication Details - {user && ( - <> - - Auth User ID: - {user.id} - - - Email: - {user.email} + {/* Header */} + + + + 👤 + + Profile + - - Email Verified: - {user.email_confirmed_at ? 'Yes' : 'No'} - - - )} - - - {/* Customer ID Card */} - - Customer Profile - - Customer ID: - {customerId || 'Not found'} - - - {/* Customer Data Card */} - {customerData && ( - - Customer Details - - Legal Name: - {customerData.legal_name || 'N/A'} - - - Birth Date: - {customerData.birth_date || 'N/A'} - - - National ID: - {customerData.national_id_number || 'N/A'} + {/* User Profile Card */} + + Account Information + {user && ( + <> + + Email: + {user.email} + + + Account Status: + + {user.email_confirmed_at ? '✓ Verified' : 'Pending Verification'} + + + + )} - - Country: - {customerData.residency_country || 'N/A'} + + {/* Support Card */} + + + 💬 + Need Help? + + + Our customer support team is available 24/7 to assist you with any questions or concerns. + + + Contact Live Support + - - Customer Type: - {customerData.customer_type || 'N/A'} + + {/* Quick Tips Card */} + + Quick Tips + + 🔒 + Keep your login credentials secure and never share them with anyone + + + 🚨 + Monitor your account regularly for any unauthorized transactions + + + ⚠️ + Our staff will never ask you to click any links or ask for your passwords + - - )} - - + + + ); } const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#f8f9fa', }, scrollView: { flex: 1, @@ -155,12 +156,31 @@ const styles = StyleSheet.create({ header: { paddingHorizontal: 24, paddingTop: 24, - paddingBottom: 16, + paddingBottom: 20, + }, + logoContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + logo: { + width: 48, + height: 48, + borderRadius: 24, + backgroundColor: 'rgba(220, 178, 78, 0.15)', + justifyContent: 'center', + alignItems: 'center', + marginRight: 16, + borderWidth: 2, + borderColor: '#dcb24e', + }, + logoText: { + fontSize: 24, }, headerTitle: { fontSize: 28, - fontWeight: 'bold', - color: '#1f2937', + fontWeight: '700', + color: '#fffffe', + letterSpacing: 0.5, }, loadingContainer: { flex: 1, @@ -168,9 +188,10 @@ const styles = StyleSheet.create({ alignItems: 'center', }, loadingText: { - fontSize: 16, - color: '#6b7280', + fontSize: 18, + color: '#fffffe', marginTop: 12, + fontWeight: '500', }, errorContainer: { flex: 1, @@ -180,57 +201,163 @@ const styles = StyleSheet.create({ }, errorText: { fontSize: 16, - color: '#ef4444', + color: '#dcb24e', textAlign: 'center', + fontWeight: '500', }, card: { - backgroundColor: '#ffffff', + backgroundColor: 'rgba(255, 255, 255, 0.95)', marginHorizontal: 24, - marginTop: 16, - padding: 20, - borderRadius: 12, - shadowColor: '#000', + marginTop: 20, + padding: 24, + borderRadius: 20, + shadowColor: '#0e273c', shadowOffset: { width: 0, - height: 2, + height: 8, }, - shadowOpacity: 0.1, - shadowRadius: 8, - elevation: 4, + shadowOpacity: 0.15, + shadowRadius: 12, + elevation: 10, + borderWidth: 1, + borderColor: 'rgba(220, 178, 78, 0.2)', }, cardTitle: { - fontSize: 18, - fontWeight: '600', - color: '#1f2937', - marginBottom: 16, + fontSize: 20, + fontWeight: '700', + color: '#0e273c', + marginBottom: 20, + letterSpacing: 0.5, }, infoRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - paddingVertical: 8, + paddingVertical: 12, borderBottomWidth: 1, - borderBottomColor: '#f3f4f6', + borderBottomColor: 'rgba(220, 178, 78, 0.1)', }, label: { - fontSize: 14, + fontSize: 15, color: '#6b7280', - fontWeight: '500', + fontWeight: '600', flex: 1, }, value: { - fontSize: 14, - color: '#1f2937', + fontSize: 15, + color: '#0e273c', flex: 2, textAlign: 'right', + fontWeight: '500', }, - valueHighlight: { - fontSize: 14, - color: '#2563eb', - fontWeight: '600', + valueVerified: { + fontSize: 15, + color: '#dcb24e', + fontWeight: '700', flex: 2, textAlign: 'right', }, + supportCard: { + backgroundColor: 'rgba(255, 255, 255, 0.95)', + marginHorizontal: 24, + marginTop: 20, + padding: 24, + borderRadius: 20, + borderWidth: 1, + borderColor: 'rgba(220, 178, 78, 0.2)', + shadowColor: '#0e273c', + shadowOffset: { + width: 0, + height: 8, + }, + shadowOpacity: 0.15, + shadowRadius: 12, + elevation: 10, + }, + supportHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 16, + }, + supportIcon: { + fontSize: 28, + marginRight: 12, + }, + supportTitle: { + fontSize: 22, + fontWeight: '700', + color: '#0e273c', + letterSpacing: 0.5, + }, + supportDescription: { + fontSize: 16, + color: '#0e273c', + lineHeight: 24, + marginBottom: 20, + fontWeight: '400', + }, + supportButton: { + backgroundColor: '#dcb24e', + paddingVertical: 16, + paddingHorizontal: 24, + borderRadius: 16, + alignItems: 'center', + shadowColor: '#dcb24e', + shadowOffset: { + width: 0, + height: 4, + }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 6, + }, + supportButtonText: { + fontSize: 16, + fontWeight: '700', + color: '#0e273c', + letterSpacing: 0.5, + }, + tipsCard: { + backgroundColor: 'rgba(255, 255, 255, 0.95)', + marginHorizontal: 24, + marginTop: 20, + padding: 24, + borderRadius: 20, + shadowColor: '#0e273c', + shadowOffset: { + width: 0, + height: 8, + }, + shadowOpacity: 0.15, + shadowRadius: 12, + elevation: 10, + borderWidth: 1, + borderColor: 'rgba(220, 178, 78, 0.2)', + }, + tipsTitle: { + fontSize: 20, + fontWeight: '700', + color: '#0e273c', + marginBottom: 20, + letterSpacing: 0.5, + }, + tipItem: { + flexDirection: 'row', + alignItems: 'flex-start', + marginBottom: 16, + }, + tipIcon: { + fontSize: 20, + marginRight: 12, + marginTop: 2, + }, + tipText: { + fontSize: 15, + color: '#0e273c', + lineHeight: 22, + flex: 1, + fontWeight: '400', + }, // debugCard: { // backgroundColor: '#f3f4f6', // marginHorizontal: 24, diff --git a/package-lock.json b/package-lock.json index 960d3ed..e7446e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9693,4 +9693,4 @@ } } } -} \ No newline at end of file +} From 12e41e4fd33faf78816a1f694254ab8be48fb3ef Mon Sep 17 00:00:00 2001 From: nictjh Date: Tue, 14 Oct 2025 13:30:50 +0800 Subject: [PATCH 2/4] Add Live Chat Support Server codes into main repo --- SupportServer/Dockerfile | 13 + SupportServer/agent_cli.py | 145 ++++++++++++ SupportServer/docker-compose.yml | 26 ++ SupportServer/main.py | 395 +++++++++++++++++++++++++++++++ SupportServer/requirements.txt | 14 ++ 5 files changed, 593 insertions(+) create mode 100644 SupportServer/Dockerfile create mode 100644 SupportServer/agent_cli.py create mode 100644 SupportServer/docker-compose.yml create mode 100644 SupportServer/main.py create mode 100644 SupportServer/requirements.txt diff --git a/SupportServer/Dockerfile b/SupportServer/Dockerfile new file mode 100644 index 0000000..98dc7a0 --- /dev/null +++ b/SupportServer/Dockerfile @@ -0,0 +1,13 @@ +FROM ubuntu:22.04 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 python3-pip tini \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY requirements.txt . +RUN pip3 install --no-cache-dir -r requirements.txt +COPY . . + +ENTRYPOINT ["/usr/bin/tini","--"] +CMD ["python3","-m","uvicorn","main:app","--host","0.0.0.0","--port","8000"] \ No newline at end of file diff --git a/SupportServer/agent_cli.py b/SupportServer/agent_cli.py new file mode 100644 index 0000000..d92f21f --- /dev/null +++ b/SupportServer/agent_cli.py @@ -0,0 +1,145 @@ +## To start a single-agent CLI, run: +## python agent_cli.py --url ws://localhost:8000/ws ## +## python3 agent_cli.py --url wss://deviation-addressing-adjust-trying.trycloudflare.com/ws +## Replace the URL with your own WebSocket endpoint. + + +import asyncio +import json +import sys +import argparse ## For command-line argument parsing +from datetime import datetime +from datetime import timezone +import contextlib + +import websockets + + +def ts_short(): + """ For timestammping use""" + return datetime.now(timezone.utc).strftime("%H:%M:%S") + + +async def stdin_reader(queue: asyncio.Queue[str]): + ## Get currently running loop, to schedule running tasks + loop = asyncio.get_running_loop() + while True: + ## This will ensure blocking the readline doesnt block the main loop + line = await loop.run_in_executor(None, sys.stdin.readline) + if line == "": + await queue.put("/quit") + break + await queue.put(line.rstrip("\n")) + + +## Run the agent CLI with given URL +async def agent_cli(url: str): + ## Append the role=agent if not present in the websocket provided + sep = "&" if "?" in url else "?" + if "role=" not in url: + url = f"{url}{sep}role=agent" + + print(f"[cli] Connecting to {url} ...") + + async with websockets.connect(url) as ws: + raw = await ws.recv() + try: + msg = json.loads(raw) + except Exception: + print("[cli] Unexpected non-JSON from server:", raw) + return + if msg.get("type") != "ready" or msg.get("role") != "agent": + print("[cli] Unexpected greeting:", msg) + return + + print("[cli] Connected as AGENT. Waiting for assignment…") + current_conv: str | None = None + + ## stdin will push typed lines into here, FIFO + inbox_q: asyncio.Queue[str] = asyncio.Queue() + stdin_task = asyncio.create_task(stdin_reader(inbox_q)) + + ## This has to run concurrently with the stdin loop + async def ws_recv_loop(): + nonlocal current_conv + while True: + raw_in = await ws.recv() + try: + data = json.loads(raw_in) + except Exception: + print("[srv] (non-JSON)", raw_in) + continue + + t = data.get("type") + ## Handling assigned messages sent in do_assign() in main.py + if t == "assigned": + current_conv = data.get("conversationId") + partner = data.get("partner", {}) + print(f"[{ts_short()}] Assigned conversation: {current_conv} (partner={partner.get('role')})") + print("Type your reply. Use /end to finish, /quit to exit.") + elif t == "message.new": + m = data.get("message", {}) + role = m.get("sender_role") + content = m.get("content") + print(f"[{ts_short()}] {role}: {content}") + elif t == "ended": + reason = data.get("reason") + print(f"[{ts_short()}] Conversation ended (reason={reason}). Waiting for next assignment…") + current_conv = None + elif t == "error": + print(f"[srv:ERROR] {data}") + else: + pass + + ## Initialise the websocket receiving loop + ws_task = asyncio.create_task(ws_recv_loop()) + + + ## Inbox processing + try: + while True: + line = await inbox_q.get() + if line.strip() == "": + continue + if line.strip().lower() == "/quit": + print("[cli] Quitting…") + break + if line.strip().lower() == "/end": + if current_conv is None: + print("[cli] No active conversation.") + continue + await ws.send(json.dumps({ + "type": "end", + "conversationId": current_conv, + })) + continue + + if current_conv is None: + print("[cli] Not assigned yet. Your message will be ignored. Use /quit to exit.") + continue + + payload = { + "type": "message.send", + "conversationId": current_conv, + "content": line, + } + await ws.send(json.dumps(payload)) + except websockets.ConnectionClosed as e: + print(f"[cli] Connection closed: {e}") + finally: + ws_task.cancel() + stdin_task.cancel() + with contextlib.suppress(Exception): + await ws.close() + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(description="Single-agent chat CLI") + parser.add_argument("--url", default="ws://localhost:8000/ws", help="WebSocket endpoint") + args = parser.parse_args() + + try: + asyncio.run(agent_cli(args.url)) + except KeyboardInterrupt: + print("[cli] Interrupted.") diff --git a/SupportServer/docker-compose.yml b/SupportServer/docker-compose.yml new file mode 100644 index 0000000..23f8eb9 --- /dev/null +++ b/SupportServer/docker-compose.yml @@ -0,0 +1,26 @@ +services: + app: + build: . + image: chat-app:latest # tag the image + container_name: chat-app + ports: + - "8000:8000" + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "python3 - <<'PY'\nimport urllib.request,sys\ntry:\n urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)\n sys.exit(0)\nexcept Exception:\n sys.exit(1)\nPY"] + interval: 10s + timeout: 5s + retries: 5 + + agent: + image: chat-app:latest # reuse same image + container_name: chat-agent + depends_on: + app: + condition: service_healthy + # INTERACTIVE TTY: + stdin_open: true # keeps STDIN open + tty: true # allocates a TTY + restart: "no" # don't auto-restart when you exit the agent + working_dir: /app + command: ["python3","agent_cli.py","--url","ws://app:8000/ws"] diff --git a/SupportServer/main.py b/SupportServer/main.py new file mode 100644 index 0000000..c668505 --- /dev/null +++ b/SupportServer/main.py @@ -0,0 +1,395 @@ +""" +FastAPI WebSocket PoC: Single-Agent live chat with in-memory FIFO queue. + + +Simplified single agent support chat, that polls the queue, when conversation ends, agent connects or customer connects + +Run the server with: + + uvicorn main:app --reload --port 8000 + +""" + +import asyncio +import contextlib +import json +import uuid +from collections import deque +from dataclasses import dataclass, field +from datetime import datetime, timezone +from typing import Dict, Optional, Any, List + + +from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query +from fastapi.responses import PlainTextResponse + + +app = FastAPI(title="WS Chat PoC", version="0.1") + + +## Data Structure for this FastAPI + +@dataclass +class Conversation: + id: str + created_at: datetime + status: str # 'waiting' | 'active' | 'closed' + customer_id: str + agent_id: Optional[str] = None + transcript: List[Dict[str, Any]] = field(default_factory=list) + + +@dataclass +class Session: + conv: Conversation + customer_ws: WebSocket + agent_ws: Optional[WebSocket] = None + customer_task: Optional[asyncio.Task] = None + agent_task: Optional[asyncio.Task] = None + + +class ChatHub: + """In-memory state for single-agent chat.""" + + def __init__(self) -> None: + ## Manage concurrency with a lock + self.lock = asyncio.Lock() + ## Declare the FIFO queue + self.waiting_queue: deque[str] = deque() + self.conversations: Dict[str, Conversation] = {} + self.sessions: Dict[str, Session] = {} + self.agent_ws: Optional[WebSocket] = None + self.agent_id: Optional[str] = None + + async def enqueue_customer(self, conv: Conversation) -> int: + """ Add conversation ID to queue under lock and returns current position. """ + async with self.lock: ## Automatially releases if theres any error + self.waiting_queue.append(conv.id) + position = len(self.waiting_queue) + return position + + async def assign_next_if_possible(self) -> Optional[str]: + """If agent present and someone is waiting, assign and return conv_id.""" + async with self.lock: + agent_ws = self.agent_ws + if agent_ws is None: ## No Agent available return None + return None + if not self.waiting_queue: ## No waiting customers + return None + + conv_id = self.waiting_queue.popleft() ## Dequeue the next customer + return conv_id + + async def set_agent(self, ws: WebSocket, agent_id: str) -> None: + """ Sets agent wwebsocket and ID under lock""" + async with self.lock: + self.agent_ws = ws + self.agent_id = agent_id + + async def clear_agent(self, ws: WebSocket) -> None: + async with self.lock: + if self.agent_ws is ws: + self.agent_ws = None + self.agent_id = None + + +# Instantiate the global chathub +hub = ChatHub() + + +## Helper functions + +async def send_json(ws: Optional[WebSocket], payload: Dict[str, Any]) -> None: + if ws is None: + return + ## Serialize and send over websocket as text + await ws.send_text(json.dumps(payload)) + + +def now_iso() -> str: + ## Timestamping helper + return datetime.now(timezone.utc).isoformat() + + +async def safe_close(ws: Optional[WebSocket]) -> None: + """Attempt to close a websocket, ignoring errors. Used for cleanup """ + if ws is None: + return + try: + await ws.close() + except Exception: + pass + + +## Core Flow of Server + +async def start_relay(session: Session) -> None: + """Start bidirectional relay tasks for the active session.""" + + if session.agent_ws is None: + # Agent isn't attached yet; wait for proper assignment + return + + async def pipe(src_ws: WebSocket, dst_ws: WebSocket, sender_role: str) -> None: + dst_role = "agent" if sender_role == "customer" else "customer" + while True: + try: + raw = await src_ws.receive_text() ## Awaits for text message from Client + msg = json.loads(raw) + except WebSocketDisconnect: + # Source left: end the conversation + await end_conversation(session.conv.id, reason=f"{sender_role}_left") + return + except Exception as e: + await send_json(src_ws, {"type": "error", "code": "BAD_JSON", "message": str(e)}) + continue + + + ## Handles the message to our simple JSON protocol + mtype = msg.get("type") + if mtype == "message.send": + content = (msg.get("content") or "").strip() + if not content: + await send_json(src_ws, {"type": "error", "code": "BAD_REQUEST", "message": "empty content"}) + continue + # Build canonical message + out = { + "type": "message.new", + "message": { + "id": str(uuid.uuid4()), ## Unique message ID for ref later + "conversationId": session.conv.id, + "sender_role": sender_role, + "content": content, + "ts": now_iso(), + }, + } + # Append to transcript + session.conv.transcript.append({ + "sender_role": sender_role, + "content": content, + "ts": out["message"]["ts"], + }) + # Relay to both sides + try: + await send_json(dst_ws, out) + except Exception: + # Partner unreachable -> end conversation + await end_conversation(session.conv.id, reason=f"{dst_role}_unreachable") + return + try: + await send_json(src_ws, out) + except Exception: + # If echoing back to sender fails, still continue; reading loop will catch disconnect + pass + + elif mtype == "end": + await end_conversation(session.conv.id, reason="closed") + return + else: + await send_json(src_ws, {"type": "error", "code": "BAD_REQUEST", "message": f"unknown type: {mtype}"}) + + # Start both directions + session.customer_task = asyncio.create_task(pipe(session.customer_ws, session.agent_ws, "customer")) + session.agent_task = asyncio.create_task(pipe(session.agent_ws, session.customer_ws, "agent")) + + +async def end_conversation(conv_id: str, reason: str) -> None: + """Close sockets, mark closed, dump transcript, and free agent to next.""" + session = hub.sessions.get(conv_id) + if session is None: + return + + # Mark closed & print transcript (hook DB save here later) + session.conv.status = "closed" + print("\n==== Conversation closed ====") + print(f"conv_id={conv_id} reason={reason} at={now_iso()}") + print(json.dumps(session.conv.transcript, indent=2)) + print("============================\n") + + # Inform both sides + payload = {"type": "ended", "conversationId": conv_id, "reason": reason} + for ws in (session.customer_ws, session.agent_ws): + try: + await send_json(ws, payload) + except Exception: + pass + + # Stop the relay loops before tearing down websockets + for task in (session.customer_task, session.agent_task): + if task is not None: + task.cancel() + for task in (session.customer_task, session.agent_task): + if task is not None: + with contextlib.suppress(asyncio.CancelledError): + await task + session.customer_task = None + session.agent_task = None + + # Close customer socket; agent stays connected unless they left + await safe_close(session.customer_ws) + + agent_ws = session.agent_ws + agent_should_close = reason in {"agent_left", "agent_unreachable"} + if agent_should_close: + await safe_close(agent_ws) + if agent_ws is not None: + await hub.clear_agent(agent_ws) + + # Cleanup + hub.sessions.pop(conv_id, None) + + # Immediately try to serve next waiting customer + next_conv_id = await hub.assign_next_if_possible() + if next_conv_id: + await do_assign(next_conv_id) + + +async def do_assign(conv_id: str) -> None: + """Pair the queued conversation with the current agent and start relaying.""" + async with hub.lock: + if hub.agent_ws is None: + # No agent anymore; requeue + hub.waiting_queue.appendleft(conv_id) + return + + ## Agent available; assign + conv = hub.conversations[conv_id] + conv.status = "active" + conv.agent_id = hub.agent_id or "agent" + # Build session + # For safety, we require the customer_ws to be attached by now via waiting_clients. + session = hub.sessions.get(conv_id) + if session is None or session.customer_ws is None: + # Customer disappeared; skip + return + session.agent_ws = hub.agent_ws + + # Notify both sides + await send_json(session.customer_ws, { + "type": "assigned", + "conversationId": conv_id, + "partner": {"role": "agent", "id": conv.agent_id}, + }) + await send_json(session.agent_ws, { + "type": "assigned", + "conversationId": conv_id, + "partner": {"role": "customer", "id": conv.customer_id}, + }) + + # Start relay loops + await start_relay(session) + + +# Track customers awaiting assignment: conv_id -> customer websocket +waiting_customers: Dict[str, WebSocket] = {} + + +# ----------------------------- Endpoints ----------------------------- # + +@app.get("/health", response_class=PlainTextResponse) +async def health() -> str: + return "ok\n" + + +@app.websocket("/ws") +async def ws_endpoint(websocket: WebSocket, role: str = Query("customer")): + """ + WebSocket entrypoint. + - role=customer | agent (no auth yet, add JWT later) + Other roles are rejected and closed nicely + """ + await websocket.accept() + + # Send immediate 'ready' with role (conversationId filled after hello for customers) + await send_json(websocket, {"type": "ready", "role": role, "conversationId": None}) + + # Agent path + if role == "agent": + agent_id = "agent-1" + await hub.set_agent(websocket, agent_id) + try: + # If anyone is waiting, assign immediately + conv_id = await hub.assign_next_if_possible() + if conv_id: + await do_assign(conv_id) + + # Keep the connection alive; agent receives events in relay + while True: + # Agent doesn't need to send 'hello'; just stay connected until disconnect + await asyncio.sleep(60) + except WebSocketDisconnect: + pass + finally: + await hub.clear_agent(websocket) + # End any active sessions with this agent + for cid, sess in list(hub.sessions.items()): + if sess.agent_ws is websocket: + await end_conversation(cid, reason="agent_left") + await safe_close(websocket) + return + + if role != "customer": + await send_json(websocket, {"type": "error", "code": "BAD_REQUEST", "message": "unknown role"}) + await safe_close(websocket) + return + + # Customer path + # Expect a 'hello' from the customer to begin + try: + raw = await websocket.receive_text() + msg = json.loads(raw) + if msg.get("type") != "hello": + raise ValueError("first message must be 'hello'") + except Exception as e: + await send_json(websocket, {"type": "error", "code": "BAD_REQUEST", "message": str(e)}) + await safe_close(websocket) + return + + # Create a new conversation and enqueue + conv_id = str(uuid.uuid4()) ## Randomly creates a unique conversation ID + conv = Conversation( + id=conv_id, + created_at=datetime.now(timezone.utc), + status="waiting", + customer_id=str(uuid.uuid4()), # PoC: random placeholder. Replace with auth.uid later + ) + hub.conversations[conv_id] = conv + + # Register this customer's socket in a pre-session holder + session = Session(conv=conv, customer_ws=websocket, agent_ws=None) + hub.sessions[conv_id] = session ## Throw into dictionary conv_id -> session + waiting_customers[conv_id] = websocket + + # Acknowledge ready with conversationId + await send_json(websocket, {"type": "ready", "role": "customer", "conversationId": conv_id}) + + position = await hub.enqueue_customer(conv) + await send_json(websocket, {"type": "queue.update", "position": position}) + + # If agent is already available, assign now + maybe_conv = await hub.assign_next_if_possible() + if maybe_conv == conv_id: + await do_assign(conv_id) + + # Keep the customer socket alive until disconnect or session relay ends it + try: + while True: + # When assigned, relay loop will handle incoming messages. + await asyncio.sleep(60) + except WebSocketDisconnect: + # If customer disconnects while waiting + if conv.status == "waiting": + # Remove from queue if present + async with hub.lock: + try: + hub.waiting_queue.remove(conv_id) + except ValueError: + pass + conv.status = "closed" + hub.sessions.pop(conv_id, None) + waiting_customers.pop(conv_id, None) + else: + # If active, end the conversation + await end_conversation(conv_id, reason="customer_left") + finally: + await safe_close(websocket) diff --git a/SupportServer/requirements.txt b/SupportServer/requirements.txt new file mode 100644 index 0000000..3dfe6b5 --- /dev/null +++ b/SupportServer/requirements.txt @@ -0,0 +1,14 @@ +annotated-types==0.7.0 +anyio==4.10.0 +click==8.3.0 +fastapi==0.116.2 +h11==0.16.0 +idna==3.10 +pydantic==2.11.9 +pydantic_core==2.33.2 +sniffio==1.3.1 +starlette==0.48.0 +typing-inspection==0.4.1 +typing_extensions==4.15.0 +uvicorn==0.35.0 +websockets==15.0.1 From af582afa5055ca3c5c11751bf4c81d4b7a23ee1d Mon Sep 17 00:00:00 2001 From: nictjh Date: Tue, 14 Oct 2025 17:17:38 +0800 Subject: [PATCH 3/4] Change UI to match everyday banks --- app/home.jsx | 181 +++++++++++++++++++------------------- supabase/.temp/cli-latest | 2 +- 2 files changed, 90 insertions(+), 93 deletions(-) diff --git a/app/home.jsx b/app/home.jsx index bfe2f71..b395046 100644 --- a/app/home.jsx +++ b/app/home.jsx @@ -245,19 +245,16 @@ export default function Home() { } return ( - + + {/* Header with Zentra Bank branding */} - + @@ -275,6 +272,15 @@ export default function Home() { {user.email} + + + + {/* White content area */} + {/* Account Balance Card */} @@ -375,25 +381,26 @@ export default function Home() { - - - + + ); } const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#0e273c', + backgroundColor: '#f8fafc', }, - gradient: { - flex: 1, + headerGradient: { + paddingBottom: 20, }, scrollView: { flex: 1, + backgroundColor: '#f8fafc', }, scrollContent: { - paddingBottom: 24, + paddingBottom: 30, + paddingTop: 10, }, headerContainer: { paddingHorizontal: 24, @@ -470,98 +477,101 @@ const styles = StyleSheet.create({ fontWeight: '600', }, balanceCard: { - backgroundColor: '#fffffe', - marginHorizontal: 24, + backgroundColor: '#ffffff', + marginHorizontal: 20, marginTop: 20, - padding: 28, - borderRadius: 20, - shadowColor: '#000', + padding: 24, + borderRadius: 12, + shadowColor: '#64748b', shadowOffset: { width: 0, - height: 8, + height: 2, }, - shadowOpacity: 0.15, - shadowRadius: 16, - elevation: 8, + shadowOpacity: 0.08, + shadowRadius: 8, + elevation: 4, borderWidth: 1, - borderColor: 'rgba(220, 178, 78, 0.1)', + borderColor: '#e2e8f0', }, balanceLabel: { - fontSize: 15, - color: '#6b7280', - marginBottom: 12, - fontWeight: '500', + fontSize: 14, + color: '#64748b', + marginBottom: 8, + fontWeight: '600', + textTransform: 'uppercase', + letterSpacing: 0.5, }, balanceAmount: { - fontSize: 36, + fontSize: 32, fontWeight: '700', - color: '#0e273c', - marginBottom: 8, + color: '#1e293b', + marginBottom: 6, letterSpacing: -0.5, }, accountNumber: { - fontSize: 15, - color: '#9ca3af', + fontSize: 14, + color: '#64748b', fontWeight: '500', }, noAccountMessage: { - fontSize: 20, + fontSize: 18, fontWeight: '700', - color: '#0e273c', + color: '#1e293b', marginBottom: 6, textAlign: 'center', }, noAccountSubtext: { - fontSize: 15, - color: '#6b7280', + fontSize: 14, + color: '#64748b', marginBottom: 20, textAlign: 'center', - lineHeight: 22, + lineHeight: 20, }, createAccountButton: { - backgroundColor: '#dcb24e', - paddingVertical: 14, - paddingHorizontal: 28, - borderRadius: 12, + backgroundColor: '#1e40af', + paddingVertical: 12, + paddingHorizontal: 24, + borderRadius: 8, alignItems: 'center', - shadowColor: '#dcb24e', + shadowColor: '#1e40af', shadowOffset: { width: 0, - height: 4, + height: 2, }, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 6, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, }, createAccountButtonText: { - color: '#fffffe', - fontSize: 16, + color: '#ffffff', + fontSize: 15, fontWeight: '600', - letterSpacing: 0.5, + letterSpacing: 0.3, }, actionsCard: { - backgroundColor: '#fffffe', - marginHorizontal: 24, - marginTop: 20, - padding: 28, - borderRadius: 20, - shadowColor: '#000', + backgroundColor: '#ffffff', + marginHorizontal: 20, + marginTop: 16, + padding: 20, + borderRadius: 12, + shadowColor: '#64748b', shadowOffset: { width: 0, - height: 8, + height: 2, }, - shadowOpacity: 0.15, - shadowRadius: 16, - elevation: 8, + shadowOpacity: 0.08, + shadowRadius: 8, + elevation: 4, borderWidth: 1, - borderColor: 'rgba(220, 178, 78, 0.1)', + borderColor: '#e2e8f0', }, actionsTitle: { - fontSize: 20, + fontSize: 16, fontWeight: '700', - color: '#0e273c', - marginBottom: 24, - letterSpacing: 0.5, + color: '#1e293b', + marginBottom: 16, + letterSpacing: 0.3, + textTransform: 'uppercase', }, actionsContainer: { flexDirection: 'row', @@ -576,12 +586,9 @@ const styles = StyleSheet.create({ flex: 1, paddingVertical: 20, paddingHorizontal: 8, - backgroundColor: 'rgba(220, 178, 78, 0.05)', - borderRadius: 16, - marginHorizontal: 4, - borderWidth: 1, - borderColor: 'rgba(220, 178, 78, 0.1)', - minHeight: 120, + backgroundColor: 'transparent', + marginHorizontal: 6, + minHeight: 110, }, applyButton: { alignItems: 'center', @@ -589,43 +596,33 @@ const styles = StyleSheet.create({ paddingVertical: 20, paddingHorizontal: 12, maxWidth: '45%', - backgroundColor: 'rgba(220, 178, 78, 0.05)', - borderRadius: 16, - marginHorizontal: 8, - borderWidth: 1, - borderColor: 'rgba(220, 178, 78, 0.1)', - minHeight: 120, + backgroundColor: 'transparent', + marginHorizontal: 6, + minHeight: 110, }, actionIconPlaceholder: { width: 56, height: 56, borderRadius: 28, - backgroundColor: 'rgba(220, 178, 78, 0.15)', + backgroundColor: '#f8fafc', justifyContent: 'center', alignItems: 'center', marginBottom: 12, - borderWidth: 2, - borderColor: 'rgba(220, 178, 78, 0.3)', - shadowColor: '#dcb24e', - shadowOffset: { - width: 0, - height: 2, - }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 2, + borderWidth: 1, + borderColor: '#e2e8f0', }, actionIconText: { - fontSize: 24, + fontSize: 22, }, actionText: { - fontSize: 12, + fontSize: 13, fontWeight: '600', - color: '#0e273c', + color: '#475569', textAlign: 'center', - letterSpacing: 0.3, + letterSpacing: 0.2, lineHeight: 16, maxWidth: '100%', flexWrap: 'wrap', + marginTop: 2, }, }); diff --git a/supabase/.temp/cli-latest b/supabase/.temp/cli-latest index 80a4412..2213dd2 100644 --- a/supabase/.temp/cli-latest +++ b/supabase/.temp/cli-latest @@ -1 +1 @@ -v2.39.2 \ No newline at end of file +v2.51.0 \ No newline at end of file From bc0f2dafeb39247adb691fb8c13bfdd6332df2c2 Mon Sep 17 00:00:00 2001 From: nictjh Date: Thu, 16 Oct 2025 13:25:16 +0800 Subject: [PATCH 4/4] Add slight padding --- .gitignore | 2 ++ app/home.jsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 67cfdf4..2c1f499 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ # dependencies node_modules/ +*.keystore + # Expo .expo/ dist/ diff --git a/app/home.jsx b/app/home.jsx index b395046..b617159 100644 --- a/app/home.jsx +++ b/app/home.jsx @@ -404,7 +404,7 @@ const styles = StyleSheet.create({ }, headerContainer: { paddingHorizontal: 24, - paddingTop: 20, + paddingTop: 40, paddingBottom: 20, }, headerTop: {