Add feature interest buttons to dares and quiz

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jamie BruntonandClaude Opus 4.5 committed 2026-01-01 18:42:56 +00:00
1 parent 3b445597f0
commit feb7657562
4 files changed
+288 -6

No files matched your search

+81 -3
View File
@@ -1,12 +1,31 @@
import { View, Text, StyleSheet, Platform } from "react-native";
import { View, Text, StyleSheet, Platform, Pressable, ActivityIndicator } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import Animated, { FadeInDown, FadeIn } from "react-native-reanimated";
import Animated, { FadeInDown, FadeIn, useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated";
import { GradientBackground } from "../../src/components/ui";
import { colors, spacing, radius, typography } from "../../src/theme";
import { colors, spacing, radius, typography, animations } from "../../src/theme";
import { useFeatureInterest } from "../../src/hooks/useFeatureInterest";
const ACCENT_COLOR = colors.premium.gold;
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
export default function DaresScreen() {
const { isInterested, isLoading, isToggling, toggleInterest, isAuthenticated } =
useFeatureInterest("dares");
const scale = useSharedValue(1);
const animatedButtonStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const handlePressIn = () => {
scale.value = withSpring(0.96, animations.spring);
};
const handlePressOut = () => {
scale.value = withSpring(1, animations.spring);
};
return (
<GradientBackground>
<View style={styles.container}>
@@ -61,6 +80,41 @@ export default function DaresScreen() {
{/* Bottom teaser */}
<Text style={styles.teaser}>Are you ready to play?</Text>
{/* Interest button */}
{isAuthenticated && !isLoading && (
<Animated.View
entering={FadeIn.delay(500).duration(400)}
style={styles.interestButtonContainer}
>
<AnimatedPressable
onPress={toggleInterest}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
disabled={isToggling}
style={[
styles.interestButton,
isInterested && styles.interestButtonActive,
animatedButtonStyle,
]}
>
{isToggling ? (
<ActivityIndicator size="small" color={ACCENT_COLOR} />
) : (
<>
<Ionicons
name={isInterested ? "checkmark-circle" : "notifications-outline"}
size={16}
color={ACCENT_COLOR}
/>
<Text style={styles.interestButtonText}>
{isInterested ? "You're on the list!" : "I'm interested"}
</Text>
</>
)}
</AnimatedPressable>
</Animated.View>
)}
</Animated.View>
</View>
</GradientBackground>
@@ -167,4 +221,28 @@ const styles = StyleSheet.create({
color: colors.textTertiary,
textAlign: 'center',
},
interestButtonContainer: {
marginTop: spacing.xl,
},
interestButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(212, 175, 55, 0.1)',
paddingHorizontal: spacing.lg,
paddingVertical: spacing.sm + 2,
borderRadius: radius.full,
borderWidth: 1,
borderColor: 'rgba(212, 175, 55, 0.2)',
gap: spacing.sm,
},
interestButtonActive: {
backgroundColor: 'rgba(212, 175, 55, 0.15)',
borderColor: 'rgba(212, 175, 55, 0.3)',
},
interestButtonText: {
...typography.subhead,
fontWeight: '600',
color: ACCENT_COLOR,
},
});
+81 -3
View File
@@ -1,12 +1,31 @@
import { View, Text, StyleSheet, Platform } from "react-native";
import { View, Text, StyleSheet, Platform, Pressable, ActivityIndicator } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import Animated, { FadeInDown, FadeIn } from "react-native-reanimated";
import Animated, { FadeInDown, FadeIn, useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated";
import { GradientBackground } from "../../src/components/ui";
import { colors, spacing, radius, typography } from "../../src/theme";
import { colors, spacing, radius, typography, animations } from "../../src/theme";
import { useFeatureInterest } from "../../src/hooks/useFeatureInterest";
const ACCENT_COLOR = colors.premium.rose;
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
export default function QuizScreen() {
const { isInterested, isLoading, isToggling, toggleInterest, isAuthenticated } =
useFeatureInterest("quiz");
const scale = useSharedValue(1);
const animatedButtonStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const handlePressIn = () => {
scale.value = withSpring(0.96, animations.spring);
};
const handlePressOut = () => {
scale.value = withSpring(1, animations.spring);
};
return (
<GradientBackground>
<View style={styles.container}>
@@ -61,6 +80,41 @@ export default function QuizScreen() {
{/* Bottom teaser */}
<Text style={styles.teaser}>Something special is being crafted for you</Text>
{/* Interest button */}
{isAuthenticated && !isLoading && (
<Animated.View
entering={FadeIn.delay(500).duration(400)}
style={styles.interestButtonContainer}
>
<AnimatedPressable
onPress={toggleInterest}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
disabled={isToggling}
style={[
styles.interestButton,
isInterested && styles.interestButtonActive,
animatedButtonStyle,
]}
>
{isToggling ? (
<ActivityIndicator size="small" color={ACCENT_COLOR} />
) : (
<>
<Ionicons
name={isInterested ? "checkmark-circle" : "notifications-outline"}
size={16}
color={ACCENT_COLOR}
/>
<Text style={styles.interestButtonText}>
{isInterested ? "You're on the list!" : "I'm interested"}
</Text>
</>
)}
</AnimatedPressable>
</Animated.View>
)}
</Animated.View>
</View>
</GradientBackground>
@@ -167,4 +221,28 @@ const styles = StyleSheet.create({
color: colors.textTertiary,
textAlign: 'center',
},
interestButtonContainer: {
marginTop: spacing.xl,
},
interestButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(232, 164, 174, 0.1)',
paddingHorizontal: spacing.lg,
paddingVertical: spacing.sm + 2,
borderRadius: radius.full,
borderWidth: 1,
borderColor: 'rgba(232, 164, 174, 0.2)',
gap: spacing.sm,
},
interestButtonActive: {
backgroundColor: 'rgba(232, 164, 174, 0.15)',
borderColor: 'rgba(232, 164, 174, 0.3)',
},
interestButtonText: {
...typography.subhead,
fontWeight: '600',
color: ACCENT_COLOR,
},
});
@@ -0,0 +1,79 @@
import { useState, useEffect, useCallback } from "react";
import { supabase } from "../lib/supabase";
import { useAuthStore } from "../store";
export function useFeatureInterest(featureName: string) {
const [isInterested, setIsInterested] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isToggling, setIsToggling] = useState(false);
const user = useAuthStore((state) => state.user);
// Fetch current interest status
useEffect(() => {
async function fetchInterest() {
if (!user?.id) {
setIsLoading(false);
return;
}
const { data } = await supabase
.from("feature_interests")
.select("id")
.eq("user_id", user.id)
.eq("feature_name", featureName)
.maybeSingle();
setIsInterested(!!data);
setIsLoading(false);
}
fetchInterest();
}, [user?.id, featureName]);
const toggleInterest = useCallback(async () => {
if (!user?.id || isToggling) return;
setIsToggling(true);
const wasInterested = isInterested;
// Optimistic update
setIsInterested(!wasInterested);
try {
if (wasInterested) {
// Remove interest
const { error } = await supabase
.from("feature_interests")
.delete()
.eq("user_id", user.id)
.eq("feature_name", featureName);
if (error) throw error;
} else {
// Add interest
const { error } = await supabase
.from("feature_interests")
.insert({
user_id: user.id,
feature_name: featureName,
});
if (error) throw error;
}
} catch (error) {
console.error("Error toggling feature interest:", error);
// Revert on error
setIsInterested(wasInterested);
} finally {
setIsToggling(false);
}
}, [user?.id, featureName, isInterested, isToggling]);
return {
isInterested,
isLoading,
isToggling,
toggleInterest,
isAuthenticated: !!user?.id,
};
}