refactor: use reusable ProfileLink component on home screen#204
refactor: use reusable ProfileLink component on home screen#204Saurav09s wants to merge 8 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors the mobile HomeScreen to render platform/profile links using a new reusable ProfileLink component, aiming to replace the previously hardcoded platform badge UI.
Changes:
- Added a new
ProfileLinkcomponent for displaying a platform + username row with an “Open” affordance. - Updated
HomeScreento render up to 4 links viaProfileLinkand show a “+N” overflow indicator. - Reorganized the HomeScreen layout around the links/QR/actions sections.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| apps/mobile/src/screens/HomeScreen.tsx | Switches platform link rendering to ProfileLink and rearranges sections, but currently introduces JSX syntax issues. |
| apps/mobile/src/components/ProfileLink.tsx | Introduces the reusable link row component used by HomeScreen. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <ProfileLink | ||
| key={link.id} | ||
| platform={platform?.name || link.platform} | ||
| username={link.username} | ||
| url={link.url} | ||
| onPress={() => Linking.openURL(link.url)} | ||
| /> |
| 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'; | ||
|
|
| marginTop: SPACING.md, | ||
| gap: SPACING.sm, | ||
| }, |
| 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', |
| padding: 16, | ||
| borderRadius: 12, | ||
| backgroundColor: '#161616', | ||
| marginBottom: 12, |
| 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 ( | ||
| <Pressable style={styles.card} onPress={handlePress}> | ||
| <View> | ||
| <Text style={styles.platform}>{platform}</Text> | ||
| <Text style={styles.username}>{username}</Text> | ||
| </View> | ||
|
|
||
| <Text style={styles.link}>Open</Text> | ||
| </Pressable> |
Signed-off-by: Saurav Suman <134825390+Saurav09s@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.
Comments suppressed due to low confidence (1)
apps/mobile/src/screens/HomeScreen.tsx:47
searchUsername/setSearchUsernameis now unused (the search UI block was removed), which will trigger the RN ESLintno-unused-varsrule and fail lint/typecheck. Either restore the search UI or remove this state (and any related imports).
const [showQR, setShowQR] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [searchUsername, setSearchUsername] = useState('');
const profileUrl = user?.defaultCardId
? `${APP_URL}/devcard/${user.defaultCardId}`
: `${APP_URL}/u/${user?.username}`;
| </TouchableOpacity> | ||
|
|
||
| {/* QR Code Section */} | ||
| {/* Action Buttons */} | ||
| <View style={styles.actions}> | ||
| <TouchableOpacity | ||
| style={styles.qrSection} | ||
| onPress={() => setShowQR(!showQR)} | ||
| style={styles.actionButton} | ||
| onPress={handleShare} |
| marginTop: SPACING.md, | ||
| gap: SPACING.sm, | ||
| }, |
| </SafeAreaView> | ||
| </View> | ||
| </ScrollView> | ||
| </SafeAreaView > |
| type ProfileLinkProps = { | ||
| platform: string; | ||
| username: string; | ||
| url: string; | ||
| onPress?: () => void; | ||
| }; | ||
| import { | ||
| COLORS, | ||
| SPACING, | ||
| FONT_SIZE, | ||
| BORDER_RADIUS, | ||
| } from '../theme/tokens'; | ||
| export default function ProfileLink({ | ||
| platform, |
| const handlePress = () => { | ||
| if (onPress) { | ||
| onPress(); | ||
| return; | ||
| } | ||
|
|
||
| Linking.openURL(url); |
| padding: SPACING.md, | ||
| borderRadius: BORDER_RADIUS.md, | ||
| backgroundColor: COLORS.bgCard, | ||
| marginBottom: SPACING.sm, |
| 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'; | ||
|
|
|
@Saurav09s There are some reviews from co-pilote, could you please look into this? |
|
@ShantKhatri can you please tell me about 1st review "The “View a DevCard” search/lookup section appears to have been removed as part of this refactor, but the PR description only mentions swapping platform badges for ProfileLink. If this feature removal is intentional, update the PR description; otherwise, please restore the search section." I think I didn't remove it. Rest all I would fix and again push |
Signed-off-by: Saurav Suman <134825390+Saurav09s@users.noreply.github.com>
| linksContainer: { | ||
| marginTop: SPACING.md, | ||
| gap: SPACING.sm, | ||
| }, | ||
| loadingRoot: { |
| {/* Platform Links */} | ||
| <View style={styles.linksContainer}> | ||
| {links.slice(0, 4).map(link => { | ||
| const platform = PLATFORMS[link.platform]; | ||
|
|
||
| return ( | ||
| <ProfileLink | ||
| key={link.id} | ||
| platform={platform?.name || link.platform} | ||
| username={link.username} | ||
| url={link.url} | ||
| /> | ||
| ); | ||
| })} | ||
| </View> |
| {/* Stats */} | ||
| <View style={styles.stats}> | ||
| <View style={styles.statItem}> | ||
| <Text style={styles.statNumber}>{links.length}</Text> | ||
| <Text style={styles.statLabel}>Links</Text> | ||
| </View> | ||
|
|
||
| {/* Search / Lookup */} | ||
| <View style={styles.searchSection}> | ||
| <Text style={styles.searchLabel}>🔍 View a DevCard</Text> | ||
| <View style={styles.searchRow}> | ||
| <TextInput | ||
| style={styles.searchInput} | ||
| placeholder="Enter username..." | ||
| placeholderTextColor={COLORS.textMuted} | ||
| value={searchUsername} | ||
| onChangeText={setSearchUsername} | ||
| autoCapitalize="none" | ||
| autoCorrect={false} | ||
| returnKeyType="go" | ||
| onSubmitEditing={() => { | ||
| const u = searchUsername.trim(); | ||
| if (u) (navigation as any).navigate('DevCardView', { username: u }); | ||
| }} | ||
| /> | ||
| <TouchableOpacity | ||
| style={styles.searchBtn} | ||
| onPress={() => { | ||
| const u = searchUsername.trim(); | ||
| if (u) (navigation as any).navigate('DevCardView', { username: u }); | ||
| }} | ||
| > | ||
| <Text style={styles.searchBtnText}>Go →</Text> | ||
| </TouchableOpacity> | ||
| </View> | ||
| <View style={styles.statDivider} /> | ||
| <View style={styles.statItem}> | ||
| <Text style={styles.statNumber}>{analytics?.totalViews || 0}</Text> | ||
| <Text style={styles.statLabel}>Views</Text> | ||
| </View> | ||
|
|
||
| {/* Stats */} | ||
| <View style={styles.stats}> | ||
| <View style={styles.statItem}> | ||
| <Text style={styles.statNumber}>{links.length}</Text> | ||
| <Text style={styles.statLabel}>Links</Text> | ||
| </View> | ||
| <View style={styles.statDivider} /> | ||
| <View style={styles.statItem}> | ||
| <Text style={styles.statNumber}>{analytics?.totalViews || 0}</Text> | ||
| <Text style={styles.statLabel}>Views</Text> | ||
| </View> | ||
| <View style={styles.statDivider} /> | ||
| <View style={styles.statItem}> | ||
| <Text style={styles.statNumber}>{analytics?.followsCount || 0}</Text> | ||
| <Text style={styles.statLabel}>Follows</Text> | ||
| </View> | ||
| <View style={styles.statDivider} /> | ||
| <View style={styles.statItem}> | ||
| <Text style={styles.statNumber}>{analytics?.followsCount || 0}</Text> | ||
| <Text style={styles.statLabel}>Follows</Text> | ||
| </View> | ||
| </ScrollView> | ||
| </SafeAreaView> | ||
| </View> | ||
| </ScrollView> | ||
| </SafeAreaView > |
| 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); | ||
| } | ||
| }; |
| 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 ( | ||
| <Pressable style={styles.card} onPress={handlePress}> | ||
| <View> | ||
| <Text style={styles.platform}>{platform}</Text> | ||
| <Text style={styles.username}>{username}</Text> | ||
| </View> | ||
|
|
||
| <Text style={styles.link}>Open</Text> | ||
| </Pressable> |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 9 comments.
Comments suppressed due to low confidence (1)
apps/mobile/src/screens/HomeScreen.tsx:57
fetchDatausestokenfor Authorization but the initialuseEffecthas an empty dependency array andfetchDatadoesn’t guard againsttokenbeing null. Iftokenis set after the first render (e.g., after login), HomeScreen won’t refetch and may send requests withBearer null. Consider returning early when!tokenand rerunning the effect whentokenchanges (or memoizingfetchDatawith[token]and depending on it).
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
const [profileRes, analyticsRes] = await Promise.all([
fetch(`${API_BASE_URL}/api/profiles/me`, {
headers: { Authorization: `Bearer ${token}` },
}),
fetch(`${API_BASE_URL}/api/analytics/overview`, {
headers: { Authorization: `Bearer ${token}` },
})
| export default function HomeScreen({ navigation }: Props) { | ||
| const { user, token } = useAuth(); | ||
| const [links, setLinks] = useState<PlatformLink[]>([]); | ||
| const [analytics, setAnalytics] = useState<any>(null); | ||
| const [showQR, setShowQR] = useState(false); | ||
| const [refreshing, setRefreshing] = useState(false); | ||
| const [loading, setLoading] = useState(true); | ||
| const [searchUsername, setSearchUsername] = useState(''); | ||
|
|
| <View style={styles.linksContainer}> | ||
| {links.slice(0, 4).map(link => { | ||
| const platform = PLATFORMS[link.platform]; | ||
|
|
||
| return ( | ||
| <ProfileLink | ||
| key={link.id} | ||
| platform={platform?.name || link.platform} | ||
| username={link.username} | ||
| url={link.url} | ||
| /> | ||
| ); | ||
| })} | ||
| </View> |
| <TouchableOpacity | ||
| style={styles.actionButton} | ||
| onPress={() => (navigation as any).navigate('DevCardView', { username: user?.username || '' })} | ||
| activeOpacity={0.85}> | ||
| <Text style={styles.actionEmoji}>👁️</Text> | ||
| <Text style={styles.actionText}>Preview</Text> | ||
| </TouchableOpacity> | ||
| </View> | ||
|
|
||
| {/* Search / Lookup */} | ||
| <View style={styles.searchSection}> | ||
| <Text style={styles.searchLabel}>🔍 View a DevCard</Text> | ||
| <View style={styles.searchRow}> | ||
| <TextInput | ||
| style={styles.searchInput} | ||
| placeholder="Enter username..." | ||
| placeholderTextColor={COLORS.textMuted} | ||
| value={searchUsername} | ||
| onChangeText={setSearchUsername} | ||
| autoCapitalize="none" | ||
| autoCorrect={false} | ||
| returnKeyType="go" | ||
| onSubmitEditing={() => { | ||
| const u = searchUsername.trim(); | ||
| if (u) (navigation as any).navigate('DevCardView', { username: u }); | ||
| }} | ||
| /> | ||
| <TouchableOpacity | ||
| style={styles.searchBtn} | ||
| onPress={() => { | ||
| const u = searchUsername.trim(); | ||
| if (u) (navigation as any).navigate('DevCardView', { username: u }); | ||
| }} | ||
| > | ||
| <Text style={styles.searchBtnText}>Go →</Text> | ||
| </TouchableOpacity> | ||
| </View> | ||
| </View> | ||
|
|
||
| {/* Stats */} | ||
| <View style={styles.stats}> |
| @@ -156,29 +134,29 @@ | |||
| {user?.bio && <Text style={styles.bio}>{user.bio}</Text>} | |||
|
|
|||
| {/* Platform Links Summary */} | |||
| marginTop: SPACING.md, | ||
| gap: SPACING.sm, | ||
| }, |
| const handlePress = async () => { | ||
| if (onPress) { | ||
| onPress(); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| await Linking.openURL(url); | ||
| } catch (error) { | ||
| console.warn('Failed to open profile link:', error); | ||
| } |
| 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 ( | ||
| <Pressable style={styles.card} onPress={handlePress}> | ||
| <View> | ||
| <Text style={styles.platform}>{platform}</Text> | ||
| <Text style={styles.username}>{username}</Text> | ||
| </View> | ||
|
|
||
| <Text style={styles.link}>Open</Text> | ||
| </Pressable> | ||
| ); |
| return ( | ||
| <ProfileLink | ||
| key={link.id} | ||
| platform={platform?.name || link.platform} | ||
| username={link.username} | ||
| url={link.url} | ||
| /> | ||
| ); |
| useEffect(() => { | ||
| fetchData(); | ||
| }, []); | ||
|
|
||
| const fetchData = async () => { | ||
| try { |
| <TouchableOpacity | ||
| style={styles.actionButton} | ||
| onPress={() => (navigation as any).navigate('DevCardView', { username: user?.username || '' })} | ||
| activeOpacity={0.85}> | ||
| <Text style={styles.actionEmoji}>👁️</Text> | ||
| <Text style={styles.actionText}>Preview</Text> | ||
| </TouchableOpacity> | ||
| </View> | ||
|
|
||
| {/* Search / Lookup */} | ||
| <View style={styles.searchSection}> | ||
| <Text style={styles.searchLabel}>🔍 View a DevCard</Text> | ||
| <View style={styles.searchRow}> | ||
| <TextInput | ||
| style={styles.searchInput} | ||
| placeholder="Enter username..." | ||
| placeholderTextColor={COLORS.textMuted} | ||
| value={searchUsername} | ||
| onChangeText={setSearchUsername} | ||
| autoCapitalize="none" | ||
| autoCorrect={false} | ||
| returnKeyType="go" | ||
| onSubmitEditing={() => { | ||
| const u = searchUsername.trim(); | ||
| if (u) (navigation as any).navigate('DevCardView', { username: u }); | ||
| }} | ||
| /> | ||
| <TouchableOpacity | ||
| style={styles.searchBtn} | ||
| onPress={() => { | ||
| const u = searchUsername.trim(); | ||
| if (u) (navigation as any).navigate('DevCardView', { username: u }); | ||
| }} | ||
| > | ||
| <Text style={styles.searchBtnText}>Go →</Text> | ||
| </TouchableOpacity> | ||
| </View> | ||
| </View> | ||
|
|
||
| {/* Stats */} | ||
| <View style={styles.stats}> |
| marginTop: SPACING.md, | ||
| gap: SPACING.sm, | ||
| }, |
| const handlePress = async () => { | ||
| if (onPress) { | ||
| onPress(); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| await Linking.openURL(url); | ||
| } catch (error) { | ||
| console.warn('Failed to open profile link:', error); | ||
| } |
|
No updates for the last few weeks, so closing this PR according to policy. |
Summary
Refactored HomeScreen to use reusable ProfileLink component.
Changes
Testing