Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 1 addition & 1 deletion android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<resources>
<string name="app_name">reflectionsprojections</string>
<string name="app_name">R|P 2025</string>
<string name="expo_system_ui_user_interface_style" translatable="false">automatic</string>
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
Expand Down
82 changes: 41 additions & 41 deletions app/(tabs)/leaderboard/leaderboard.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -42,23 +42,28 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject<any> })
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<LeaderboardListHandle>(null);
Expand All @@ -74,18 +79,12 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject<any> })
}, []);

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(
Expand Down Expand Up @@ -113,22 +112,27 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject<any> })
}),
).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,
Expand All @@ -146,15 +150,13 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject<any> })
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)),
showTopSeparator: false,
topSeparatorIndex: -1,
peopleAboveCount: 0,
showBottomSeparator: false,
bottomSeparatorIndex: -1,
peopleBelowCount: 0,
};
}
Expand All @@ -164,7 +166,6 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject<any> })
const contextEnd = Math.min(mappedData.length, userIndex + CONTEXT_AFTER + 1);
const userContext = mappedData.slice(contextStart, contextEnd);

// Deduplicate overlaps
const seen = new Set<string>();
const pushUnique = (arr: typeof mappedData, into: typeof mappedData) => {
for (const item of arr) {
Expand All @@ -179,25 +180,20 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject<any> })
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,
showTopSeparator,
topSeparatorIndex,
peopleAboveCount,
showBottomSeparator,
bottomSeparatorIndex,
peopleBelowCount,
};
}, [activeTab, dailyLeaderboard, globalLeaderboard, attendee?.userId]);
Expand All @@ -208,7 +204,6 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject<any> })
ref={outerScrollRef}
showsVerticalScrollIndicator={false}
headerMaxHeight={330}
// No load-more scrolling
renderHeaderNavBarComponent={() => (
<HeaderNavBar isHeader={true} showTint={false}>
<Header title={'STANDINGS'} bigText={false} />
Expand Down Expand Up @@ -348,8 +343,15 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject<any> })
</View>

<FadeInWrapper delay={1000}>
{activeTab === 0 &&
(!dailyLeaderboard || (dailyLeaderboard.leaderboard?.length ?? 0) === 0) ? (
{activeTab === 0 && dailyLoading && !dailyLeaderboard.leaderboard.length ? (
<View style={{ paddingVertical: 40, alignItems: 'center' }}>
<ActivityIndicator size="large" color="#ffffff" />
</View>
) : activeTab === 1 && globalLoading && !globalLeaderboard.leaderboard.length ? (
<View style={{ paddingVertical: 40, alignItems: 'center' }}>
<ActivityIndicator size="large" color="#ffffff" />
</View>
) : activeTab === 0 && (!dailyLeaderboard || (dailyLeaderboard.leaderboard?.length ?? 0) === 0) ? (
<View style={{ paddingVertical: 40, alignItems: 'center' }}>
<Text
style={{
Expand All @@ -362,8 +364,7 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject<any> })
No leaderboard for today — check back tomorrow!
</Text>
</View>
) : activeTab === 1 &&
(!globalLeaderboard || (globalLeaderboard.leaderboard?.length ?? 0) === 0) ? (
) : activeTab === 1 && (!globalLeaderboard || (globalLeaderboard.leaderboard?.length ?? 0) === 0) ? (
<View style={{ paddingVertical: 40, alignItems: 'center' }}>
<Text
style={{
Expand All @@ -386,17 +387,16 @@ const LeaderboardScreen = ({ scrollRef }: { scrollRef?: React.RefObject<any> })
topSeparatorIndex={topSeparatorIndex}
peopleAboveCount={peopleAboveCount}
showBottomSeparator={showBottomSeparator}
bottomSeparatorIndex={bottomSeparatorIndex}
peopleBelowCount={peopleBelowCount}
isLoading={activeTab === 0 ? dailyLoading : globalLoading}
/>
</View>
)}
</FadeInWrapper>

<View style={{ height: 100 }} />
</AnimatedScrollView>
</View>
);
};

export default LeaderboardScreen;
export default LeaderboardScreen;
10 changes: 4 additions & 6 deletions app/(tabs)/scanner/scanner_user.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<keyof Attendee | null>(null);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -116,4 +114,4 @@ export default function ScannerScreen() {
</View>
</SafeAreaView>
);
}
}
Loading