mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
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:
1 parent
3b445597f0
commit
feb7657562
4 files changed
+288
-6
No files matched your search
@@ -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.
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in new issue
Block a user