This commit is contained in:
jjbrunton committed 2026-01-01 16:43:55 +00:00
1 parent b4c8a2f303
commit 0fa7971cb9
5 files changed
+266 -22

No files matched your search

+1
View File
@@ -1,6 +1,7 @@
apply plugin: "com.android.application" apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android" apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react" apply plugin: "com.facebook.react"
apply plugin: "com.google.gms.google-services"
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath() def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
+2 -1
View File
@@ -2,7 +2,7 @@
buildscript { buildscript {
ext { ext {
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '35.0.0' buildToolsVersion = findProperty('android.buildToolsVersion') ?: '34.0.0'
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24') minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24')
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35') compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '35') targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '35')
@@ -18,6 +18,7 @@ buildscript {
classpath('com.android.tools.build:gradle') classpath('com.android.tools.build:gradle')
classpath('com.facebook.react:react-native-gradle-plugin') classpath('com.facebook.react:react-native-gradle-plugin')
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin') classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
classpath('com.google.gms:google-services:4.4.2')
} }
} }
+1
View File
@@ -30,6 +30,7 @@
"backgroundColor": "#1a1a2e" "backgroundColor": "#1a1a2e"
}, },
"package": "com.sauci.app", "package": "com.sauci.app",
"googleServicesFile": "./google-services.json",
"permissions": [ "permissions": [
"RECEIVE_BOOT_COMPLETED", "RECEIVE_BOOT_COMPLETED",
"VIBRATE", "VIBRATE",
+223 -21
View File
@@ -35,35 +35,65 @@ interface PlayTabButtonProps {
children?: React.ReactNode; children?: React.ReactNode;
onPress?: (e: any) => void; onPress?: (e: any) => void;
accessibilityState?: { selected?: boolean }; accessibilityState?: { selected?: boolean };
isMenuOpen?: boolean;
onToggleMenu?: () => void;
} }
function PlayTabButton({ onPress, accessibilityState }: PlayTabButtonProps) { interface RadialMenuItem {
id: string;
icon: keyof typeof Ionicons.glyphMap;
label: string;
angle: number; // degrees from top (0 = top, -45 = left, 45 = right)
color?: string;
}
const RADIAL_MENU_ITEMS: RadialMenuItem[] = [
{ id: 'dares', icon: 'flash', label: 'Dares', angle: -50, color: '#FF6B35' },
{ id: 'match', icon: 'flame', label: 'Match', angle: 0 },
{ id: 'quiz', icon: 'help-circle', label: 'Quiz', angle: 50, color: '#7B68EE' },
];
const RADIAL_DISTANCE = 100; // Distance from center button
// Store for sharing state between PlayTabButton and AppLayout
let globalMenuToggle: (() => void) | null = null;
let globalMenuOpen = false;
function PlayTabButton({ accessibilityState }: PlayTabButtonProps) {
const isSelected = accessibilityState?.selected; const isSelected = accessibilityState?.selected;
const [, forceUpdate] = useState({});
// Subscribe to global state changes
useEffect(() => {
const interval = setInterval(() => {
forceUpdate({});
}, 50);
return () => clearInterval(interval);
}, []);
return ( return (
<Pressable <View style={styles.playButtonContainer}>
onPress={onPress}
style={styles.playButtonContainer}
>
{/* Outer glow effect */} {/* Outer glow effect */}
<View style={[styles.playButtonGlow, isSelected && styles.playButtonGlowActive]} /> <View style={[styles.playButtonGlow, isSelected && styles.playButtonGlowActive]} />
{/* Circle container */} {/* Main circle button */}
<View style={styles.playButtonCircle}> <Pressable onPress={() => globalMenuToggle?.()}>
<LinearGradient <Animated.View style={styles.playButtonCircle}>
colors={gradients.primary as [string, string]} <LinearGradient
start={{ x: 0, y: 0 }} colors={gradients.primary as [string, string]}
end={{ x: 1, y: 1 }} start={{ x: 0, y: 0 }}
style={styles.playButtonGradient} end={{ x: 1, y: 1 }}
> style={styles.playButtonGradient}
<Ionicons >
name="flame" <Ionicons
size={32} name={globalMenuOpen ? "close" : "flame"}
color={colors.text} size={32}
/> color={colors.text}
</LinearGradient> />
</View> </LinearGradient>
</Pressable> </Animated.View>
</Pressable>
</View>
); );
} }
@@ -83,6 +113,60 @@ export default function AppLayout() {
const hasCheckedInitialBiometric = useRef(false); const hasCheckedInitialBiometric = useRef(false);
const wentToBackgroundAt = useRef<number | null>(null); const wentToBackgroundAt = useRef<number | null>(null);
// Radial menu state
const [isRadialMenuOpen, setIsRadialMenuOpen] = useState(false);
const radialMenuAnim = useRef(new Animated.Value(0)).current;
const toggleRadialMenu = useCallback(() => {
const toValue = isRadialMenuOpen ? 0 : 1;
setIsRadialMenuOpen(!isRadialMenuOpen);
globalMenuOpen = !isRadialMenuOpen;
Animated.spring(radialMenuAnim, {
toValue,
useNativeDriver: true,
tension: 100,
friction: 8,
}).start();
}, [isRadialMenuOpen, radialMenuAnim]);
const closeRadialMenu = useCallback(() => {
setIsRadialMenuOpen(false);
globalMenuOpen = false;
Animated.timing(radialMenuAnim, {
toValue: 0,
duration: 150,
useNativeDriver: true,
}).start();
}, [radialMenuAnim]);
const handleRadialMenuItemPress = useCallback((itemId: string) => {
closeRadialMenu();
// Navigate based on item
switch (itemId) {
case 'match':
router.push('/(app)/swipe');
break;
case 'dares':
// TODO: Navigate to dares screen
console.log('Dares pressed - not yet implemented');
break;
case 'quiz':
// TODO: Navigate to quiz screen
console.log('Quiz pressed - not yet implemented');
break;
}
}, [router, closeRadialMenu]);
// Register global menu toggle for PlayTabButton
useEffect(() => {
globalMenuToggle = toggleRadialMenu;
return () => {
globalMenuToggle = null;
};
}, [toggleRadialMenu]);
// Check if we're on screens that should hide the tab bar // Check if we're on screens that should hide the tab bar
const segmentStrings = segments as string[]; const segmentStrings = segments as string[];
const isOnOnboarding = segmentStrings.includes("onboarding"); const isOnOnboarding = segmentStrings.includes("onboarding");
@@ -527,6 +611,78 @@ export default function AppLayout() {
/> />
</Tabs> </Tabs>
{/* Radial Menu Overlay */}
{isRadialMenuOpen && (
<Pressable
style={radialStyles.overlay}
onPress={closeRadialMenu}
/>
)}
{/* Radial Menu Items - positioned above the center button */}
<Animated.View
style={[
radialStyles.menuContainer,
{ opacity: radialMenuAnim },
]}
pointerEvents={isRadialMenuOpen ? 'auto' : 'none'}
>
{RADIAL_MENU_ITEMS.map((item) => {
// Convert angle to radians and calculate position
const angleRad = (item.angle - 90) * (Math.PI / 180); // -90 to make 0 = top
const translateX = Math.cos(angleRad) * RADIAL_DISTANCE;
const translateY = Math.sin(angleRad) * RADIAL_DISTANCE;
const itemScale = radialMenuAnim.interpolate({
inputRange: [0, 1],
outputRange: [0.3, 1],
});
const itemTranslateX = radialMenuAnim.interpolate({
inputRange: [0, 1],
outputRange: [0, translateX],
});
const itemTranslateY = radialMenuAnim.interpolate({
inputRange: [0, 1],
outputRange: [0, translateY],
});
return (
<Animated.View
key={item.id}
style={[
radialStyles.menuItem,
{
transform: [
{ translateX: itemTranslateX },
{ translateY: itemTranslateY },
{ scale: itemScale },
],
},
]}
>
<Pressable
onPress={() => handleRadialMenuItemPress(item.id)}
style={radialStyles.menuItemButton}
>
<View style={[
radialStyles.menuItemCircle,
item.color ? { backgroundColor: item.color } : null,
]}>
<Ionicons
name={item.icon}
size={22}
color={colors.text}
/>
</View>
<Text style={radialStyles.menuItemLabel}>{item.label}</Text>
</Pressable>
</Animated.View>
);
})}
</Animated.View>
{/* Match Notification Overlay - Using Animated.View instead of Modal for Android compatibility */} {/* Match Notification Overlay - Using Animated.View instead of Modal for Android compatibility */}
{matchNotification && ( {matchNotification && (
<Animated.View <Animated.View
@@ -756,3 +912,49 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
}, },
}); });
// Radial menu styles
const TAB_BAR_HEIGHT = Platform.OS === 'ios' ? 88 : 64;
const radialStyles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.6)',
zIndex: 998,
},
menuContainer: {
position: 'absolute',
bottom: TAB_BAR_HEIGHT + 30, // Position above tab bar, centered on button
left: 0,
right: 0,
alignItems: 'center',
zIndex: 999,
},
menuItem: {
position: 'absolute',
alignItems: 'center',
},
menuItemButton: {
alignItems: 'center',
},
menuItemCircle: {
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: colors.primary,
justifyContent: 'center',
alignItems: 'center',
...shadows.lg,
borderWidth: 2,
borderColor: 'rgba(255, 255, 255, 0.2)',
},
menuItemLabel: {
...typography.caption1,
color: colors.text,
marginTop: 6,
fontWeight: '600',
textShadowColor: 'rgba(0, 0, 0, 0.5)',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 2,
},
});
+39
View File
@@ -0,0 +1,39 @@
{
"project_info": {
"project_number": "764866133492",
"project_id": "sauci-6840e",
"storage_bucket": "sauci-6840e.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:764866133492:android:eabdb78009a18d2ffe4da1",
"android_client_info": {
"package_name": "com.sauci.app"
}
},
"oauth_client": [
{
"client_id": "764866133492-e78o2rh3rdjc4rfj2j6r2j77b065kggm.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDOAqIXAd_dy7L9ZflPvNkJdNXWMaGZ1O4"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "764866133492-e78o2rh3rdjc4rfj2j6r2j77b065kggm.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}