From 6e3f79a23138e470f321a06348c55cc9cc462011 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sat, 20 Dec 2025 19:36:54 +0530 Subject: [PATCH] updated contribution page to show special mentions --- .gitignore | 4 +- src/screens/ContributorsScreen.tsx | 529 ++++++++++++++++++++++++----- 2 files changed, 445 insertions(+), 88 deletions(-) diff --git a/.gitignore b/.gitignore index bec2a69b..c243a6e5 100644 --- a/.gitignore +++ b/.gitignore @@ -31,9 +31,9 @@ yarn-error.* *.pem # local env files -.env -.env*.local +.env*.local +.env # Sentry ios/sentry.properties android/sentry.properties diff --git a/src/screens/ContributorsScreen.tsx b/src/screens/ContributorsScreen.tsx index def265b5..9932555d 100644 --- a/src/screens/ContributorsScreen.tsx +++ b/src/screens/ContributorsScreen.tsx @@ -12,13 +12,14 @@ import { Linking, RefreshControl, FlatList, - ActivityIndicator + ActivityIndicator, + Alert } from 'react-native'; import { mmkvStorage } from '../services/mmkvStorage'; import { useNavigation } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import FastImage from '@d11/react-native-fast-image'; -import { Feather } from '@expo/vector-icons'; +import { Feather, FontAwesome5 } from '@expo/vector-icons'; import { useTheme } from '../contexts/ThemeContext'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { fetchContributors, GitHubContributor } from '../services/githubReleaseService'; @@ -30,6 +31,48 @@ const isLargeTablet = width >= 1024; const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; +// Discord API URL from environment +const DISCORD_USER_API = process.env.EXPO_PUBLIC_DISCORD_USER_API || 'https://pfpfinder.com/api/discord/user'; + +// Discord brand color +const DISCORD_BRAND_COLOR = '#5865F2'; + +// Special mentions - Discord community members (only store IDs and roles) +interface SpecialMentionConfig { + discordId: string; + role: string; + description: string; +} + +interface DiscordUserData { + id: string; + global_name: string | null; + username: string; + avatar: string | null; +} + +interface SpecialMention extends SpecialMentionConfig { + name: string; + username: string; + avatarUrl: string; + isLoading: boolean; +} + +const SPECIAL_MENTIONS_CONFIG: SpecialMentionConfig[] = [ + { + discordId: '709281623866081300', + role: 'Community Manager', + description: 'Manages the Discord & Reddit communities for Nuvio', + }, + { + discordId: '777773947071758336', + role: 'Server Sponsor', + description: 'Sponsored the server infrastructure for Nuvio', + }, +]; + +type TabType = 'contributors' | 'special'; + interface ContributorCardProps { contributor: GitHubContributor; currentTheme: any; @@ -86,15 +129,174 @@ const ContributorCard: React.FC = ({ contributor, currentT ); }; +// Special Mention Card Component - Same layout as ContributorCard +interface SpecialMentionCardProps { + mention: SpecialMention; + currentTheme: any; + isTablet: boolean; + isLargeTablet: boolean; +} + +const SpecialMentionCard: React.FC = ({ mention, currentTheme, isTablet, isLargeTablet }) => { + const handlePress = useCallback(() => { + // Try to open Discord profile + const discordUrl = `discord://-/users/${mention.discordId}`; + Linking.canOpenURL(discordUrl).then((supported) => { + if (supported) { + Linking.openURL(discordUrl); + } else { + // Fallback: show alert with Discord info + Alert.alert( + mention.name, + `Discord: @${mention.username}\n\nOpen Discord and search for this user to connect with them.`, + [{ text: 'OK' }] + ); + } + }); + }, [mention.discordId, mention.name, mention.username]); + + // Default avatar fallback + const defaultAvatar = `https://cdn.discordapp.com/embed/avatars/0.png`; + + return ( + + {/* Avatar with Discord badge */} + + {mention.isLoading ? ( + + + + ) : ( + + )} + + + + + + {/* User info */} + + + {mention.isLoading ? 'Loading...' : mention.name} + + {!mention.isLoading && mention.username && ( + + @{mention.username} + + )} + + + {mention.role} + + + + + {/* Discord icon on right */} + + + ); +}; + const ContributorsScreen: React.FC = () => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); const insets = useSafeAreaInsets(); - + + const [activeTab, setActiveTab] = useState('contributors'); const [contributors, setContributors] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(null); + const [specialMentions, setSpecialMentions] = useState([]); + const [specialMentionsLoading, setSpecialMentionsLoading] = useState(true); + + // Fetch Discord user data for special mentions + const loadSpecialMentions = useCallback(async () => { + setSpecialMentionsLoading(true); + + // Initialize with loading state + const initialMentions: SpecialMention[] = SPECIAL_MENTIONS_CONFIG.map(config => ({ + ...config, + name: 'Loading...', + username: '', + avatarUrl: '', + isLoading: true, + })); + setSpecialMentions(initialMentions); + + // Fetch each user's data from Discord API + const fetchedMentions = await Promise.all( + SPECIAL_MENTIONS_CONFIG.map(async (config): Promise => { + try { + const response = await fetch(`${DISCORD_USER_API}/${config.discordId}`); + if (!response.ok) { + throw new Error('Failed to fetch Discord user'); + } + const userData: DiscordUserData = await response.json(); + + return { + ...config, + name: userData.global_name || userData.username, + username: userData.username, + avatarUrl: userData.avatar || '', + isLoading: false, + }; + } catch (error) { + if (__DEV__) console.error(`Error fetching Discord user ${config.discordId}:`, error); + // Return fallback data + return { + ...config, + name: 'Discord User', + username: config.discordId, + avatarUrl: '', + isLoading: false, + }; + } + }) + ); + + setSpecialMentions(fetchedMentions); + setSpecialMentionsLoading(false); + }, []); + + // Load special mentions when switching to that tab + useEffect(() => { + if (activeTab === 'special' && specialMentions.length === 0) { + loadSpecialMentions(); + } + }, [activeTab, specialMentions.length, loadSpecialMentions]); const loadContributors = useCallback(async (isRefresh = false) => { try { @@ -104,7 +306,7 @@ const ContributorsScreen: React.FC = () => { setLoading(true); } setError(null); - + // Check cache first (unless refreshing) if (!isRefresh) { try { @@ -112,7 +314,7 @@ const ContributorsScreen: React.FC = () => { const cacheTimestamp = await mmkvStorage.getItem('github_contributors_timestamp'); const now = Date.now(); const ONE_HOUR = 60 * 60 * 1000; // 1 hour cache - + if (cachedData && cacheTimestamp) { const timestamp = parseInt(cacheTimestamp, 10); if (now - timestamp < ONE_HOUR) { @@ -136,10 +338,10 @@ const ContributorsScreen: React.FC = () => { try { await mmkvStorage.removeItem('github_contributors'); await mmkvStorage.removeItem('github_contributors_timestamp'); - } catch {} + } catch { } } } - + const data = await fetchContributors(); if (data && Array.isArray(data) && data.length > 0) { setContributors(data); @@ -155,7 +357,7 @@ const ContributorsScreen: React.FC = () => { try { await mmkvStorage.removeItem('github_contributors'); await mmkvStorage.removeItem('github_contributors_timestamp'); - } catch {} + } catch { } setError('Unable to load contributors. This might be due to GitHub API rate limits.'); } } catch (err) { @@ -184,7 +386,7 @@ const ContributorsScreen: React.FC = () => { if (__DEV__) console.error('Error checking cache on mount:', error); } }; - + clearInvalidCache(); loadContributors(); }, [loadContributors]); @@ -247,7 +449,7 @@ const ContributorsScreen: React.FC = () => { { backgroundColor: currentTheme.colors.darkBackground } ]}> - + { + {/* Tab Switcher */} + + setActiveTab('contributors')} + activeOpacity={0.7} + > + + Contributors + + + setActiveTab('special')} + activeOpacity={0.7} + > + + Special Mentions + + + + - {error ? ( - - - - {error} - - - GitHub API rate limit exceeded. Please try again later or pull to refresh. - - loadContributors()} - > - - Try Again - - - - ) : contributors.length === 0 ? ( - - - - No contributors found - - - ) : ( - - } - showsVerticalScrollIndicator={false} - > - - - - - We're grateful for every contribution - - - Each line of code, bug report, and suggestion helps make Nuvio better for everyone - - - - - + {error ? ( + + + + {error} + + + GitHub API rate limit exceeded. Please try again later or pull to refresh. + + loadContributors()} + > + + Try Again + + + + ) : contributors.length === 0 ? ( + + + + No contributors found + + + ) : ( + + } + showsVerticalScrollIndicator={false} + > + + + + + We're grateful for every contribution + + + Each line of code, bug report, and suggestion helps make Nuvio better for everyone + + + + + + + )} + + ) : ( + // Special Mentions Tab + - + > + + + + + Special Thanks + + + These amazing people help keep the Nuvio community running and the servers online + + + + + {specialMentions.map((mention: SpecialMention) => ( + + ))} + )} @@ -563,6 +856,70 @@ const styles = StyleSheet.create({ externalIcon: { marginLeft: 8, }, + // Special Mentions - Compact styles for horizontal layout + specialAvatarContainer: { + position: 'relative', + marginRight: 16, + }, + discordBadgeSmall: { + position: 'absolute', + bottom: -2, + right: -2, + width: 20, + height: 20, + borderRadius: 10, + alignItems: 'center', + justifyContent: 'center', + borderWidth: 2, + borderColor: '#1a1a1a', + }, + roleBadgeSmall: { + paddingHorizontal: 8, + paddingVertical: 3, + borderRadius: 10, + marginTop: 4, + alignSelf: 'flex-start', + }, + roleBadgeText: { + fontSize: 10, + fontWeight: '600', + textTransform: 'uppercase', + letterSpacing: 0.3, + }, + // Tab Switcher Styles + tabSwitcher: { + flexDirection: 'row', + marginHorizontal: 16, + marginBottom: 16, + padding: 4, + borderRadius: 12, + }, + tabletTabSwitcher: { + marginHorizontal: 32, + marginBottom: 24, + padding: 6, + borderRadius: 16, + }, + tab: { + flex: 1, + paddingVertical: 10, + paddingHorizontal: 16, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center', + }, + tabletTab: { + paddingVertical: 14, + paddingHorizontal: 24, + borderRadius: 12, + }, + tabText: { + fontSize: 14, + fontWeight: '600', + }, + tabletTabText: { + fontSize: 16, + }, }); export default ContributorsScreen;