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,
+},
});