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

+47
View File
@@ -0,0 +1,47 @@
---
description: Security check, commit with sensible message, and push
allowed-tools: Bash(git status:*), Bash(git diff:*), Bash(git log:*), Bash(git add:*), Bash(git commit:*), Bash(git push:*), Bash(git branch:*)
---
## Context
- Current branch: !`git branch --show-current`
- Git status: !`git status --short`
- Staged changes: !`git diff --cached --stat`
- Unstaged changes: !`git diff --stat`
## Your Task
Perform a secure commit and push with the following steps:
### Step 1: Security Check
Before committing, scan the staged changes for potential secrets or sensitive data:
- API keys, tokens, or credentials (patterns like `sk-`, `api_key`, `secret`, `password`, `token`, `bearer`)
- `.env` files or environment variable files with actual values
- Private keys or certificates
- Database connection strings with credentials
- Hardcoded URLs with authentication parameters
Run `git diff --cached` to inspect what will be committed. If you find ANY potential secrets:
1. STOP immediately
2. List the specific files and lines containing potential secrets
3. Do NOT proceed with the commit
4. Suggest how to fix (use environment variables, add to .gitignore, etc.)
### Step 2: Stage All Changes
If security check passes, stage all changes with `git add -A`.
### Step 3: Generate Commit Message
Analyze the changes and create a concise, meaningful commit message that:
- Summarizes what changed (not how)
- Uses imperative mood ("Add feature" not "Added feature")
- Is 50 chars or less for the subject line
- Includes the standard Claude Code footer
Look at recent commits with `git log --oneline -5` to match the repository's commit style.
### Step 4: Commit and Push
1. Create the commit with the generated message
2. Push to the remote repository
If any step fails, report the error and stop.
+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 { 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 { 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 ACCENT_COLOR = colors.premium.gold;
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
export default function DaresScreen() { 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 ( return (
<GradientBackground> <GradientBackground>
<View style={styles.container}> <View style={styles.container}>
@@ -61,6 +80,41 @@ export default function DaresScreen() {
{/* Bottom teaser */} {/* Bottom teaser */}
<Text style={styles.teaser}>Are you ready to play?</Text> <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> </Animated.View>
</View> </View>
</GradientBackground> </GradientBackground>
@@ -167,4 +221,28 @@ const styles = StyleSheet.create({
color: colors.textTertiary, color: colors.textTertiary,
textAlign: 'center', 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 { 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 { 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 ACCENT_COLOR = colors.premium.rose;
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
export default function QuizScreen() { 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 ( return (
<GradientBackground> <GradientBackground>
<View style={styles.container}> <View style={styles.container}>
@@ -61,6 +80,41 @@ export default function QuizScreen() {
{/* Bottom teaser */} {/* Bottom teaser */}
<Text style={styles.teaser}>Something special is being crafted for you</Text> <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> </Animated.View>
</View> </View>
</GradientBackground> </GradientBackground>
@@ -167,4 +221,28 @@ const styles = StyleSheet.create({
color: colors.textTertiary, color: colors.textTertiary,
textAlign: 'center', 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,
};
}