diff --git a/apps/mobile/src/components/ProfileLink.tsx b/apps/mobile/src/components/ProfileLink.tsx new file mode 100644 index 00000000..20067d64 --- /dev/null +++ b/apps/mobile/src/components/ProfileLink.tsx @@ -0,0 +1,82 @@ +// components/ProfileLink.tsx + +import React from 'react'; +import { + Linking, + Pressable, + Text, + View, + StyleSheet, +} from 'react-native'; +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 = async () => { + if (onPress) { + onPress(); + return; + } + + try { + await Linking.openURL(url); + } catch (error) { + console.warn('Failed to open profile link:', error); + } + }; + + return ( + + + {platform} + {username} + + + Open + + ); +} + +const styles = StyleSheet.create({ + card: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + padding: SPACING.md, + borderRadius: BORDER_RADIUS.md, + backgroundColor: COLORS.bgCard, + }, + + platform: { + fontSize: FONT_SIZE.md, + fontWeight: '600', + color: COLORS.textPrimary, + }, + + username: { + marginTop: SPACING.xs, + fontSize: FONT_SIZE.sm, + color: COLORS.textMuted, + }, + + link: { + fontSize: FONT_SIZE.sm, + fontWeight: '600', + color: COLORS.primary, + }, +}); \ No newline at end of file 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 diff --git a/apps/mobile/src/screens/HomeScreen.tsx b/apps/mobile/src/screens/HomeScreen.tsx index 99c5cb58..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'; @@ -20,7 +18,7 @@ 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'; type Props = { navigation: NativeStackNavigationProp; }; @@ -39,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 + 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`, { @@ -67,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); @@ -93,21 +86,6 @@ export default function HomeScreen({ navigation }: Props) { } }; - if (loading) { - return ( - - - - - - - - - - - ); - } - return ( @@ -156,29 +134,29 @@ export default function HomeScreen({ navigation }: Props) { {user?.bio && {user.bio}} {/* Platform Links Summary */} - - {links.length > 0 ? ( - <> - {links.slice(0, 4).map(link => { - const platform = PLATFORMS[link.platform]; - return ( - - - {platform?.name || link.platform} - - - ); - })} - {links.length > 4 && ( - - +{links.length - 4} - - )} - - ) : ( - No platform links added yet. Add links in the Links tab to populate your preview. - )} + {/* Platform Links */} + + {links.slice(0, 4).map(link => { + const platform = PLATFORMS[link.platform]; + + return ( + + ); + })} + + {links.length > 4 && ( + + + +{links.length - 4} + + + )} {/* QR Code Section */} @@ -231,36 +209,6 @@ export default function HomeScreen({ navigation }: Props) { - {/* Search / Lookup */} - - 🔍 View a DevCard - - { - const u = searchUsername.trim(); - if (u) (navigation as any).navigate('DevCardView', { username: u }); - }} - /> - { - const u = searchUsername.trim(); - if (u) (navigation as any).navigate('DevCardView', { username: u }); - }} - > - Go → - - - - {/* Stats */} @@ -357,48 +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 }, - 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 }, + linksContainer: { + marginTop: SPACING.md, + gap: SPACING.sm, +}, });