From 420629845d8e7bb73e3b7bd439b75eec33d69f27 Mon Sep 17 00:00:00 2001 From: Saurav09s Date: Thu, 21 May 2026 00:11:55 +0530 Subject: [PATCH 1/6] refactor: use reusable ProfileLink component on home screen --- apps/mobile/src/components/ProfileLink.tsx | 71 +++++++++ apps/mobile/src/screens/HomeScreen.tsx | 158 +++++++++++---------- 2 files changed, 155 insertions(+), 74 deletions(-) create mode 100644 apps/mobile/src/components/ProfileLink.tsx diff --git a/apps/mobile/src/components/ProfileLink.tsx b/apps/mobile/src/components/ProfileLink.tsx new file mode 100644 index 00000000..3f2f1558 --- /dev/null +++ b/apps/mobile/src/components/ProfileLink.tsx @@ -0,0 +1,71 @@ +// components/ProfileLink.tsx + +import React from 'react'; +import { + Linking, + Pressable, + Text, + View, + StyleSheet, +} from 'react-native'; + +type ProfileLinkProps = { + platform: string; + username: string; + url: string; + onPress?: () => void; +}; + +export default function ProfileLink({ + platform, + username, + url, + onPress, +}: ProfileLinkProps) { + const handlePress = () => { + if (onPress) { + onPress(); + return; + } + + Linking.openURL(url); + }; + + return ( + + + {platform} + {username} + + + Open + + ); +} + +const styles = StyleSheet.create({ + card: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + padding: 16, + borderRadius: 12, + backgroundColor: '#161616', + marginBottom: 12, + }, + platform: { + fontSize: 16, + fontWeight: '600', + color: '#ffffff', + }, + username: { + marginTop: 4, + fontSize: 14, + color: '#a1a1aa', + }, + link: { + fontSize: 14, + fontWeight: '600', + color: '#4f8cff', + }, +}); \ No newline at end of file diff --git a/apps/mobile/src/screens/HomeScreen.tsx b/apps/mobile/src/screens/HomeScreen.tsx index 80de203c..9295bf7a 100644 --- a/apps/mobile/src/screens/HomeScreen.tsx +++ b/apps/mobile/src/screens/HomeScreen.tsx @@ -18,6 +18,8 @@ import { PLATFORMS } from '@devcard/shared'; import { APP_URL, API_BASE_URL } from '../config'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RootStackParamList } from '../navigation/MainTabs'; +import ProfileLink from '../components/ProfileLink'; +import { Linking } from 'react-native'; type Props = { navigation: NativeStackNavigationProp; @@ -38,7 +40,7 @@ export default function HomeScreen({ navigation }: Props) { const [showQR, setShowQR] = useState(false); const [refreshing, setRefreshing] = useState(false); - const profileUrl = user?.defaultCardId + const profileUrl = user?.defaultCardId ? `${APP_URL}/devcard/${user.defaultCardId}` : `${APP_URL}/u/${user?.username}`; @@ -133,95 +135,99 @@ export default function HomeScreen({ navigation }: Props) { {user?.bio && {user.bio}} - {/* Platform Links Summary */} - + {/* Platform Links */} + {links.slice(0, 4).map(link => { const platform = PLATFORMS[link.platform]; + return ( - - - {platform?.name || link.platform} - - + Linking.openURL(link.url)} + /> ); })} - {links.length > 4 && ( - - +{links.length - 4} - - )} + {links.length > 4 && ( + + +{links.length - 4} + + )} + - {/* QR Code Section */} + {/* QR Code Section */} + setShowQR(!showQR)} + activeOpacity={0.85}> + {showQR ? ( + + + Scan to open your DevCard + + ) : ( + + 📱 + Tap to show QR code + + )} + + + {/* Action Buttons */} + setShowQR(!showQR)} + style={styles.actionButton} + onPress={handleShare} activeOpacity={0.85}> - {showQR ? ( - - - Scan to open your DevCard - - ) : ( - - 📱 - Tap to show QR code - - )} + 📤 + Share Card - {/* Action Buttons */} - - - 📤 - Share Card - + (navigation as any).navigate('Views')} + activeOpacity={0.85}> + 📈 + Analytics + - (navigation as any).navigate('Views')} - activeOpacity={0.85}> - 📈 - Analytics - + (navigation as any).navigate('DevCardView', { username: user?.username || '' })} + activeOpacity={0.85}> + 👁️ + Preview + + - (navigation as any).navigate('DevCardView', { username: user?.username || '' })} - activeOpacity={0.85}> - 👁️ - Preview - + {/* Stats */} + + + {links.length} + Links - - {/* Stats */} - - - {links.length} - Links - - - - {analytics?.totalViews || 0} - Views - - - - {analytics?.followsCount || 0} - Follows - + + + {analytics?.totalViews || 0} + Views + + + + {analytics?.followsCount || 0} + Follows - - + + + ); } @@ -299,4 +305,8 @@ const styles = StyleSheet.create({ statNumber: { fontSize: FONT_SIZE.xl, fontWeight: '800', color: COLORS.primary }, statLabel: { fontSize: FONT_SIZE.xs, color: COLORS.textMuted, marginTop: 4 }, statDivider: { width: 1, backgroundColor: COLORS.border }, + linksContainer: { + marginTop: SPACING.md, + gap: SPACING.sm, +}, }); From 40a4a08992acf7f729a63efdecdb9f9438fb1a88 Mon Sep 17 00:00:00 2001 From: Saurav09s Date: Thu, 21 May 2026 00:37:17 +0530 Subject: [PATCH 2/6] refactor: remove redundant onPress prop usage --- apps/mobile/src/components/ProfileLink.tsx | 32 ++++++++++++++-------- apps/mobile/src/screens/HomeScreen.tsx | 3 +- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/apps/mobile/src/components/ProfileLink.tsx b/apps/mobile/src/components/ProfileLink.tsx index 3f2f1558..4cc5f8c8 100644 --- a/apps/mobile/src/components/ProfileLink.tsx +++ b/apps/mobile/src/components/ProfileLink.tsx @@ -15,7 +15,12 @@ type ProfileLinkProps = { url: string; onPress?: () => void; }; - +import { + COLORS, + SPACING, + FONT_SIZE, + BORDER_RADIUS, +} from '../theme/tokens'; export default function ProfileLink({ platform, username, @@ -48,24 +53,27 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - padding: 16, - borderRadius: 12, - backgroundColor: '#161616', - marginBottom: 12, + padding: SPACING.md, + borderRadius: BORDER_RADIUS.md, + backgroundColor: COLORS.bgCard, + marginBottom: SPACING.sm, }, + platform: { - fontSize: 16, + fontSize: FONT_SIZE.md, fontWeight: '600', - color: '#ffffff', + color: COLORS.textPrimary, }, + username: { - marginTop: 4, - fontSize: 14, - color: '#a1a1aa', + marginTop: SPACING.xs, + fontSize: FONT_SIZE.sm, + color: COLORS.textMuted, }, + link: { - fontSize: 14, + fontSize: FONT_SIZE.sm, fontWeight: '600', - color: '#4f8cff', + color: COLORS.primary, }, }); \ No newline at end of file diff --git a/apps/mobile/src/screens/HomeScreen.tsx b/apps/mobile/src/screens/HomeScreen.tsx index 9295bf7a..c840ff24 100644 --- a/apps/mobile/src/screens/HomeScreen.tsx +++ b/apps/mobile/src/screens/HomeScreen.tsx @@ -19,7 +19,7 @@ import { APP_URL, API_BASE_URL } from '../config'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RootStackParamList } from '../navigation/MainTabs'; import ProfileLink from '../components/ProfileLink'; -import { Linking } from 'react-native'; + type Props = { navigation: NativeStackNavigationProp; @@ -146,7 +146,6 @@ export default function HomeScreen({ navigation }: Props) { platform={platform?.name || link.platform} username={link.username} url={link.url} - onPress={() => Linking.openURL(link.url)} /> ); })} From 9ed8f49417c3a35bb099b05a9cbba72764504437 Mon Sep 17 00:00:00 2001 From: Saurav09s Date: Sat, 23 May 2026 22:50:00 +0530 Subject: [PATCH 3/6] fix: handle Linking.openURL failures in ProfileLink --- apps/mobile/src/components/ProfileLink.tsx | 33 ++++++++++++---------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/apps/mobile/src/components/ProfileLink.tsx b/apps/mobile/src/components/ProfileLink.tsx index 4cc5f8c8..22dc6e04 100644 --- a/apps/mobile/src/components/ProfileLink.tsx +++ b/apps/mobile/src/components/ProfileLink.tsx @@ -8,33 +8,37 @@ import { View, StyleSheet, } from 'react-native'; - -type ProfileLinkProps = { - platform: string; - username: string; - url: string; - onPress?: () => void; -}; import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS, } from '../theme/tokens'; +type ProfileLinkProps = { + platform: string; + username: string; + url: string; + onPress?: () => void; +}; + export default function ProfileLink({ platform, username, url, onPress, }: ProfileLinkProps) { - const handlePress = () => { - if (onPress) { - onPress(); - return; - } +const handlePress = async () => { + if (onPress) { + onPress(); + return; + } - Linking.openURL(url); - }; + try { + await Linking.openURL(url); + } catch (error) { + console.warn('Failed to open profile link:', error); + } +}; return ( @@ -56,7 +60,6 @@ const styles = StyleSheet.create({ padding: SPACING.md, borderRadius: BORDER_RADIUS.md, backgroundColor: COLORS.bgCard, - marginBottom: SPACING.sm, }, platform: { From 32f667654044be7180016f469c03fa7755bd8048 Mon Sep 17 00:00:00 2001 From: Saurav09s Date: Sat, 23 May 2026 22:53:53 +0530 Subject: [PATCH 4/6] fix: handle Linking.openURL failures in ProfileLink --- apps/mobile/src/screens/HomeScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/src/screens/HomeScreen.tsx b/apps/mobile/src/screens/HomeScreen.tsx index 7a88a41f..35585fd2 100644 --- a/apps/mobile/src/screens/HomeScreen.tsx +++ b/apps/mobile/src/screens/HomeScreen.tsx @@ -248,7 +248,7 @@ export default function HomeScreen({ navigation }: Props) { - + ); } From ce911458f99c79ee4690d41048cb7959a40e1db2 Mon Sep 17 00:00:00 2001 From: Saurav09s Date: Sat, 23 May 2026 23:09:43 +0530 Subject: [PATCH 5/6] refactor: scope HomeScreen changes to ProfileLink usage --- apps/mobile/src/components/ProfileLink.tsx | 22 +-- apps/mobile/src/screens/HomeScreen.tsx | 209 +++++++-------------- 2 files changed, 83 insertions(+), 148 deletions(-) diff --git a/apps/mobile/src/components/ProfileLink.tsx b/apps/mobile/src/components/ProfileLink.tsx index 22dc6e04..20067d64 100644 --- a/apps/mobile/src/components/ProfileLink.tsx +++ b/apps/mobile/src/components/ProfileLink.tsx @@ -27,18 +27,18 @@ export default function ProfileLink({ url, onPress, }: ProfileLinkProps) { -const handlePress = async () => { - if (onPress) { - onPress(); - return; - } + const handlePress = async () => { + if (onPress) { + onPress(); + return; + } - try { - await Linking.openURL(url); - } catch (error) { - console.warn('Failed to open profile link:', error); - } -}; + try { + await Linking.openURL(url); + } catch (error) { + console.warn('Failed to open profile link:', error); + } + }; return ( diff --git a/apps/mobile/src/screens/HomeScreen.tsx b/apps/mobile/src/screens/HomeScreen.tsx index 35585fd2..317cbe18 100644 --- a/apps/mobile/src/screens/HomeScreen.tsx +++ b/apps/mobile/src/screens/HomeScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect } from 'react'; import { View, Text, @@ -9,9 +9,7 @@ import { StatusBar, Image, RefreshControl, - TextInput, } from 'react-native'; -import { Skeleton } from '../components/Skeleton'; import { SafeAreaView } from 'react-native-safe-area-context'; import QRCode from 'react-native-qrcode-svg'; import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS, SHADOWS } from '../theme/tokens'; @@ -21,8 +19,6 @@ import { APP_URL, API_BASE_URL } from '../config'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RootStackParamList } from '../navigation/MainTabs'; import ProfileLink from '../components/ProfileLink'; - - type Props = { navigation: NativeStackNavigationProp; }; @@ -41,15 +37,16 @@ export default function HomeScreen({ navigation }: Props) { const [analytics, setAnalytics] = useState(null); const [showQR, setShowQR] = useState(false); const [refreshing, setRefreshing] = useState(false); - const [loading, setLoading] = useState(true); - const [searchUsername, setSearchUsername] = useState(''); const profileUrl = user?.defaultCardId ? `${APP_URL}/devcard/${user.defaultCardId}` : `${APP_URL}/u/${user?.username}`; - const fetchData = useCallback(async () => { - setLoading(true); + useEffect(() => { + fetchData(); + }, []); + + const fetchData = async () => { try { const [profileRes, analyticsRes] = await Promise.all([ fetch(`${API_BASE_URL}/api/profiles/me`, { @@ -69,14 +66,8 @@ export default function HomeScreen({ navigation }: Props) { } } catch (err) { console.error('Failed to fetch dashboard data:', err); - } finally { - setLoading(false); } - }, [token]); - - useEffect(() => { - fetchData(); - }, [fetchData]); + }; const onRefresh = async () => { setRefreshing(true); @@ -95,21 +86,6 @@ export default function HomeScreen({ navigation }: Props) { } }; - if (loading) { - return ( - - - - - - - - - - - ); - } - return ( @@ -157,6 +133,7 @@ export default function HomeScreen({ navigation }: Props) { {user?.bio && {user.bio}} + {/* Platform Links Summary */} {/* Platform Links */} {links.slice(0, 4).map(link => { @@ -172,82 +149,84 @@ export default function HomeScreen({ navigation }: Props) { ); })} + {links.length > 4 && ( - +{links.length - 4} + + +{links.length - 4} + )} - - {/* QR Code Section */} - setShowQR(!showQR)} - activeOpacity={0.85}> - {showQR ? ( - - - Scan to open your DevCard - - ) : ( - - 📱 - Tap to show QR code - - )} - - - {/* Action Buttons */} - + {/* QR Code Section */} setShowQR(!showQR)} activeOpacity={0.85}> - 📤 - Share Card + {showQR ? ( + + + Scan to open your DevCard + + ) : ( + + 📱 + Tap to show QR code + + )} - (navigation as any).navigate('Views')} - activeOpacity={0.85}> - 📈 - Analytics - + {/* Action Buttons */} + + + 📤 + Share Card + - (navigation as any).navigate('DevCardView', { username: user?.username || '' })} - activeOpacity={0.85}> - 👁️ - Preview - - + (navigation as any).navigate('Views')} + activeOpacity={0.85}> + 📈 + Analytics + - {/* Stats */} - - - {links.length} - Links - - - - {analytics?.totalViews || 0} - Views + (navigation as any).navigate('DevCardView', { username: user?.username || '' })} + activeOpacity={0.85}> + 👁️ + Preview + - - - {analytics?.followsCount || 0} - Follows + + {/* Stats */} + + + {links.length} + Links + + + + {analytics?.totalViews || 0} + Views + + + + {analytics?.followsCount || 0} + Follows + - - + ); } @@ -330,48 +309,4 @@ const styles = StyleSheet.create({ marginTop: SPACING.md, gap: SPACING.sm, }, - loadingRoot: { - flex: 1, - padding: SPACING.lg, - backgroundColor: COLORS.bgPrimary, - }, - loadingSpacer: { - marginTop: SPACING.sm, - }, - loadingSection: { - marginTop: SPACING.lg, - }, - emptyHint: { - color: COLORS.textMuted, - fontSize: FONT_SIZE.sm, - lineHeight: 20, - marginTop: SPACING.sm, - maxWidth: '70%', - }, - // Search - searchSection: { - marginBottom: SPACING.lg, - }, - searchLabel: { - fontSize: FONT_SIZE.sm, fontWeight: '700', color: COLORS.textSecondary, - marginBottom: SPACING.sm, letterSpacing: 0.3, - }, - searchRow: { - flexDirection: 'row', gap: SPACING.sm, - }, - searchInput: { - flex: 1, - backgroundColor: COLORS.bgCard, - borderRadius: BORDER_RADIUS.md, - paddingHorizontal: SPACING.md, paddingVertical: 12, - color: COLORS.textPrimary, fontSize: FONT_SIZE.md, - borderWidth: 1, borderColor: COLORS.border, - }, - searchBtn: { - backgroundColor: COLORS.primary, - borderRadius: BORDER_RADIUS.md, - paddingHorizontal: SPACING.lg, - justifyContent: 'center', alignItems: 'center', - }, - searchBtnText: { color: COLORS.white, fontWeight: '700', fontSize: FONT_SIZE.md }, }); From 89b39f82916b8b6a9b99b9a8e1e2c0d0cf5c0146 Mon Sep 17 00:00:00 2001 From: Saurav09s Date: Sat, 23 May 2026 23:11:43 +0530 Subject: [PATCH 6/6] test: add ProfileLink component tests --- .../components/__tests__/ProfileLink.test.tsx | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 apps/mobile/src/components/__tests__/ProfileLink.test.tsx diff --git a/apps/mobile/src/components/__tests__/ProfileLink.test.tsx b/apps/mobile/src/components/__tests__/ProfileLink.test.tsx new file mode 100644 index 00000000..a9e557f7 --- /dev/null +++ b/apps/mobile/src/components/__tests__/ProfileLink.test.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react-native'; +import ProfileLink from '../ProfileLink'; + +describe('ProfileLink', () => { + it('renders platform and username', () => { + const { getByText } = render( + + ); + + expect(getByText('GitHub')).toBeTruthy(); + expect(getByText('@saurav')).toBeTruthy(); + }); + + it('calls custom onPress handler when provided', () => { + const mockPress = jest.fn(); + + const { getByText } = render( + + ); + + fireEvent.press(getByText('Open')); + + expect(mockPress).toHaveBeenCalledTimes(1); + }); +}); \ No newline at end of file