diff --git a/android/app/build.gradle b/android/app/build.gradle index 1cb0224..bba3770 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -99,7 +99,7 @@ android { applicationId 'com.reflectionsprojections' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 3 + versionCode 5 versionName "1.0.0" } signingConfigs { diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index 7fae0cc..0000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 78aaf45..0000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 7a0f085..0000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 730e3fa..0000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index b11a322..0000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 4842897..aeb672d 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - reflectionsprojections + R|P 2025 automatic contain false diff --git a/app/(tabs)/leaderboard/leaderboard.tsx b/app/(tabs)/leaderboard/leaderboard.tsx index 77770ba..47dbe45 100644 --- a/app/(tabs)/leaderboard/leaderboard.tsx +++ b/app/(tabs)/leaderboard/leaderboard.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef, useCallback } from 'react'; -import { View, PanResponder, Animated, Pressable, Text } from 'react-native'; +import { View, PanResponder, Animated, Pressable, Text, Dimensions, ActivityIndicator } from 'react-native'; import { ThemedText } from '@/components/themed/ThemedText'; import { Header } from '@/components/home/Header'; import { @@ -42,23 +42,28 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject }) today.getDate(), ).padStart(2, '0')}`; + const dailyLoading = useAppSelector((state) => state.leaderboard.daily.status === 'loading'); + const globalLoading = useAppSelector((state) => state.leaderboard.global.status === 'loading'); + React.useEffect(() => { + if (!attendee?.userId) return; + if (!dailyLeaderboard.day || dailyLeaderboard.day !== dayStr) { dispatch(fetchDailyLeaderboard({ day: dayStr })); } if (globalLeaderboard.leaderboard.length === 0) { dispatch(fetchGlobalLeaderboard({})); } - }, [dayStr]); + }, [dayStr, attendee?.userId]); const dailyUserRank = - dailyLeaderboard.leaderboard.find((x) => x.userId === attendee?.userId)?.rank ?? 0; + dailyLeaderboard?.leaderboard?.find((x) => x.userId === attendee?.userId)?.rank ?? 0; const globalUserRank = - globalLeaderboard.leaderboard.find((x) => x.userId === attendee?.userId)?.rank ?? 0; + globalLeaderboard?.leaderboard?.find((x) => x.userId === attendee?.userId)?.rank ?? 0; const dailyPoints = - dailyLeaderboard.leaderboard.find((x) => x.userId === attendee?.userId)?.points ?? 0; + dailyLeaderboard?.leaderboard?.find((x) => x.userId === attendee?.userId)?.points ?? 0; const globalPoints = - globalLeaderboard.leaderboard.find((x) => x.userId === attendee?.userId)?.points ?? 0; + globalLeaderboard?.leaderboard?.find((x) => x.userId === attendee?.userId)?.points ?? 0; const pan = useRef(new Animated.ValueXY()).current; const listRef = useRef(null); @@ -74,18 +79,12 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject }) }, []); const handleRankPress = async () => { - await triggerIfEnabled(hapticsEnabled, 'medium'); - listRef.current?.scrollToUser(); - - const userIndex = data.findIndex((p: any) => p.userId === attendee?.userId); - - if (userIndex !== -1 && outerScrollRef.current) { - const ITEM_HEIGHT = 94; - const HEADER_APPROX = 0; - const scrollY = HEADER_APPROX + userIndex * ITEM_HEIGHT; - try { - outerScrollRef.current.scrollTo({ y: scrollY, animated: true }); - } catch {} + try { + await triggerIfEnabled(hapticsEnabled, 'medium'); + // Use the ref to call the method on the child component + listRef.current?.scrollToUser(); + } catch (error) { + console.error('handleRankPress error:', error); } }; const panResponder = useRef( @@ -113,22 +112,27 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject }) }), ).current; - // No load-more. We'll compute top/user/bottom sections with separators. - const { data, showTopSeparator, topSeparatorIndex, peopleAboveCount, showBottomSeparator, - bottomSeparatorIndex, peopleBelowCount, } = React.useMemo(() => { const src = activeTab === 0 - ? (dailyLeaderboard.leaderboard ?? dailyLeaderboard.leaderboard) - : (globalLeaderboard.leaderboard ?? globalLeaderboard.leaderboard); - if (!src) return { data: [], showSeparator: false, separatorIndex: -1, peopleBetweenCount: 0 }; + ? (dailyLeaderboard?.leaderboard ?? []) + : (globalLeaderboard?.leaderboard ?? []); + if (!src || src.length === 0) + return { + data: [], + showTopSeparator: false, + topSeparatorIndex: -1, + peopleAboveCount: 0, + showBottomSeparator: false, + peopleBelowCount: 0, + }; const mappedData = src.map((p) => ({ rank: p.rank, @@ -146,7 +150,6 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject }) const CONTEXT_AFTER = 6; const BOTTOM_COUNT = 20; - // If no user found or list small, just show up to TOP_COUNT if (userIndex === -1 || mappedData.length <= TOP_COUNT) { return { data: mappedData.slice(0, Math.min(TOP_COUNT, mappedData.length)), @@ -154,7 +157,6 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject }) topSeparatorIndex: -1, peopleAboveCount: 0, showBottomSeparator: false, - bottomSeparatorIndex: -1, peopleBelowCount: 0, }; } @@ -164,7 +166,6 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject }) const contextEnd = Math.min(mappedData.length, userIndex + CONTEXT_AFTER + 1); const userContext = mappedData.slice(contextStart, contextEnd); - // Deduplicate overlaps const seen = new Set(); const pushUnique = (arr: typeof mappedData, into: typeof mappedData) => { for (const item of arr) { @@ -179,17 +180,13 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject }) pushUnique(top, assembled); pushUnique(userContext, assembled); - // Count everyone before the user's context - const peopleAboveCount = Math.max(0, contextStart); - // Count everyone after the user's context + const peopleAboveCount = Math.max(0, contextStart - TOP_COUNT); const peopleBelowCount = Math.max(0, mappedData.length - contextEnd); const showTopSeparator = peopleAboveCount > 0; const topSeparatorIndex = showTopSeparator ? top.length : -1; const showBottomSeparator = peopleBelowCount > 0; - // Place the bottom separator after the user context (end of assembled array) - const bottomSeparatorIndex = showBottomSeparator ? assembled.length : -1; return { data: assembled, @@ -197,7 +194,6 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject }) topSeparatorIndex, peopleAboveCount, showBottomSeparator, - bottomSeparatorIndex, peopleBelowCount, }; }, [activeTab, dailyLeaderboard, globalLeaderboard, attendee?.userId]); @@ -208,7 +204,6 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject }) ref={outerScrollRef} showsVerticalScrollIndicator={false} headerMaxHeight={330} - // No load-more scrolling renderHeaderNavBarComponent={() => (
@@ -348,8 +343,15 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject }) - {activeTab === 0 && - (!dailyLeaderboard || (dailyLeaderboard.leaderboard?.length ?? 0) === 0) ? ( + {activeTab === 0 && dailyLoading && !dailyLeaderboard.leaderboard.length ? ( + + + + ) : activeTab === 1 && globalLoading && !globalLeaderboard.leaderboard.length ? ( + + + + ) : activeTab === 0 && (!dailyLeaderboard || (dailyLeaderboard.leaderboard?.length ?? 0) === 0) ? ( }) No leaderboard for today — check back tomorrow! - ) : activeTab === 1 && - (!globalLeaderboard || (globalLeaderboard.leaderboard?.length ?? 0) === 0) ? ( + ) : activeTab === 1 && (!globalLeaderboard || (globalLeaderboard.leaderboard?.length ?? 0) === 0) ? ( }) topSeparatorIndex={topSeparatorIndex} peopleAboveCount={peopleAboveCount} showBottomSeparator={showBottomSeparator} - bottomSeparatorIndex={bottomSeparatorIndex} peopleBelowCount={peopleBelowCount} + isLoading={activeTab === 0 ? dailyLoading : globalLoading} /> )} - ); }; -export default LeaderboardScreen; +export default LeaderboardScreen; \ No newline at end of file diff --git a/app/(tabs)/scanner/scanner_user.tsx b/app/(tabs)/scanner/scanner_user.tsx index 920e72a..09a9bd1 100644 --- a/app/(tabs)/scanner/scanner_user.tsx +++ b/app/(tabs)/scanner/scanner_user.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { SafeAreaView, View, Dimensions, Text, TouchableOpacity } from 'react-native'; +import { SafeAreaView, View, Dimensions, Text, TouchableOpacity, ActivityIndicator } from 'react-native'; import BackgroundSvg from '@/assets/images/qrbackground.svg'; import { useQRCode } from '@/hooks/useQRCode'; import QRDisplay from '@/components/scanner/QRDisplay'; @@ -8,7 +8,7 @@ import { getWeekday } from '@/lib/utils'; import { Attendee } from '@/api/types'; const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window'); -const QR_SIZE = SCREEN_WIDTH * 0.67; +const QR_SIZE = SCREEN_WIDTH * 0.6; // Slightly smaller to fit better export default function ScannerScreen() { const [weekdayShort, setWeekdayShort] = useState(null); @@ -36,15 +36,13 @@ export default function ScannerScreen() { setIsRefreshing(true); handleManualRefresh(); - // Set cooldown for 3 seconds after a brief delay setTimeout(() => { setIsRefreshing(false); setIsRefreshCooldown(true); - setTimeout(() => { setIsRefreshCooldown(false); }, 3000); - }, 500); // Small delay to prevent flashing + }, 500); }, [isRefreshing, isRefreshCooldown, handleManualRefresh]); const attendee = useAppSelector((state) => state.attendee.attendee); @@ -116,4 +114,4 @@ export default function ScannerScreen() { ); -} +} \ No newline at end of file diff --git a/components/leaderboard/LeaderboardList.tsx b/components/leaderboard/LeaderboardList.tsx index 9c7a248..0f3db0a 100644 --- a/components/leaderboard/LeaderboardList.tsx +++ b/components/leaderboard/LeaderboardList.tsx @@ -16,15 +16,12 @@ interface LeaderboardData { interface LeaderboardListProps { data: LeaderboardData[]; userId: string; - // Top separator showTopSeparator?: boolean; topSeparatorIndex?: number; peopleAboveCount?: number; - - // Bottom separator showBottomSeparator?: boolean; - bottomSeparatorIndex?: number; peopleBelowCount?: number; + isLoading: boolean; } export type LeaderboardListHandle = { @@ -41,6 +38,7 @@ export const LeaderboardList = forwardRef { + if (listRef.current) { + listRef.current.scrollToIndex({ + index: userIndex, + animated: true, + viewPosition: 0.5, + }); + } + }, 100); + } } }; @@ -65,14 +74,30 @@ export const LeaderboardList = forwardRef + + + ); + } + + if (!isLoading && data.length === 0) { + return ( + + No leaderboard data available. + + ); + } + return ( String(item.userId)} - // 👇 Android-only adjustments - scrollEnabled={Platform.OS === 'android' ? true : false} - removeClippedSubviews={Platform.OS === 'android' ? false : true} + scrollEnabled={true} + removeClippedSubviews={Platform.OS === 'android'} initialScrollIndex={ Platform.OS === 'android' ? undefined @@ -94,6 +119,17 @@ export const LeaderboardList = forwardRef { + // This ensures a robust scroll, especially on Android + const wait = new Promise(resolve => setTimeout(resolve, 500)); + wait.then(() => { + try { + listRef.current?.scrollToIndex({ index: info.index, animated: true }); + } catch (e) { + console.log("Scroll to index failed again: ", e); + } + }); + }} renderItem={({ item, index }) => { const shouldShowTopSeparator = showTopSeparator && index === topSeparatorIndex; @@ -123,7 +159,7 @@ export const LeaderboardList = forwardRef - {peopleAboveCount > 0 ? `...` : 'Your Position'} + {peopleAboveCount > 0 ? `...${peopleAboveCount} People Above` : 'Your Position'} )} - {/* Bottom separator moved to ListFooterComponent */} {Platform.OS === 'android' ? ( ); }} - onScrollToIndexFailed={() => { - setTimeout(scrollToUser, 100); - }} ListFooterComponent={() => showBottomSeparator ? ( - {`...`} + {`...${peopleBelowCount} People Below`} ); }, -); +); \ No newline at end of file diff --git a/components/scanner/QRDisplay.tsx b/components/scanner/QRDisplay.tsx index cd0cab8..35783a2 100644 --- a/components/scanner/QRDisplay.tsx +++ b/components/scanner/QRDisplay.tsx @@ -22,6 +22,14 @@ const QRDisplay: React.FC = ({ qrSize, }) => { const MAX_RETRY_ATTEMPTS = 3; + const QR_ROTATION_DEG = '12.5deg'; // Rotation for the QR code and its placement + // These multipliers will need to be fine-tuned to fit YOUR specific SVG flag + // The 'qrSize' prop is typically the ideal square size for the QR code itself. + // The container needs to be slightly larger to account for the flag's visual size. + const containerWidthMultiplier = 1.05; // Adjust to visually fit the flag's width + const containerHeightMultiplier = 0.9; // Adjust to visually fit the flag's height + const qrCodeSizeMultiplier = 0.8; // Adjust QR code size relative to the flag container + const containerPadding = 15; // Padding inside the flag where the QR code sits if (loading) { return ( @@ -55,11 +63,36 @@ const QRDisplay: React.FC = ({ if (qrValue) { return ( + {/* + This View now simply positions the QR code, applying the tilt. + It does NOT have its own background color, allowing the SVG flag + to show through. + The width, height, and padding are crucial for alignment with your SVG. + */} - + ); @@ -68,4 +101,4 @@ const QRDisplay: React.FC = ({ return null; }; -export default QRDisplay; +export default QRDisplay; \ No newline at end of file diff --git a/package.json b/package.json index 9988007..1a974b6 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@gorhom/bottom-sheet": "^5.2.6", "@react-native-async-storage/async-storage": "2.1.2", "@react-native-community/blur": "^4.4.1", - "@react-native-firebase/app": "^23.0.1", + "@react-native-firebase/app": "^23.1.2", "@react-native-firebase/messaging": "^23.0.1", "@react-native-google-signin/google-signin": "^15.0.0", "@react-native-picker/picker": "^2.11.1",