mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Add profile photo to onboarding flow
This commit is contained in:
1 parent
349f460e15
commit
b070a6f6a2
15 files changed
+731
-210
No files matched your search
@@ -1,17 +1,18 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, Text, TouchableOpacity, Switch, Platform } from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
withSpring,
|
||||
useSharedValue,
|
||||
FadeIn,
|
||||
} from 'react-native-reanimated';
|
||||
import { ShimmerEffect } from './ui/ShimmerEffect';
|
||||
import { colors, gradients, spacing, radius, typography, blur, shadows, animations } from '../theme';
|
||||
import type { QuestionPack } from '../types';
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, Text, TouchableOpacity, Platform } from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
withSpring,
|
||||
useSharedValue,
|
||||
FadeIn,
|
||||
} from 'react-native-reanimated';
|
||||
import { ShimmerEffect } from './ui/ShimmerEffect';
|
||||
import { GlassToggle } from './ui/GlassToggle';
|
||||
import { colors, gradients, spacing, radius, typography, blur, shadows, animations } from '../theme';
|
||||
import type { QuestionPack } from '../types';
|
||||
|
||||
// Boutique gradient palette for pack cards
|
||||
const BOUTIQUE_GRADIENTS: Array<readonly [string, string]> = [
|
||||
@@ -147,23 +148,17 @@ export function BoutiquePackCard({
|
||||
<View style={styles.footer}>
|
||||
<View style={styles.questionBadge}>
|
||||
<Text style={styles.questionCount}>{questionCount}</Text>
|
||||
<Text style={styles.questionLabel}>questions</Text>
|
||||
</View>
|
||||
|
||||
<Switch
|
||||
value={isEnabled}
|
||||
onValueChange={onToggle}
|
||||
disabled={isPremiumLocked}
|
||||
trackColor={{
|
||||
false: 'rgba(255, 255, 255, 0.15)',
|
||||
true: pack.is_premium ? colors.premium.gold : colors.primary,
|
||||
}}
|
||||
thumbColor={colors.text}
|
||||
ios_backgroundColor="rgba(255, 255, 255, 0.15)"
|
||||
style={isPremiumLocked ? { opacity: 0.4 } : undefined}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.questionLabel}>questions</Text>
|
||||
</View>
|
||||
|
||||
<GlassToggle
|
||||
value={isEnabled}
|
||||
onValueChange={onToggle}
|
||||
disabled={isPremiumLocked}
|
||||
style={isPremiumLocked ? { opacity: 0.4 } : undefined}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Lock overlay for premium locked packs */}
|
||||
{isPremiumLocked && (
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { StyleSheet, Pressable, ViewStyle, Platform } from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withSpring,
|
||||
interpolateColor,
|
||||
} from 'react-native-reanimated';
|
||||
import { colors, gradients, radius, animations } from '../../theme';
|
||||
|
||||
// Haptics is not supported on web
|
||||
const triggerHaptic = async () => {
|
||||
if (Platform.OS === 'web') return;
|
||||
const Haptics = await import('expo-haptics');
|
||||
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
};
|
||||
|
||||
interface GlassToggleProps {
|
||||
value: boolean;
|
||||
onValueChange: (value: boolean) => void;
|
||||
disabled?: boolean;
|
||||
style?: ViewStyle;
|
||||
}
|
||||
|
||||
const TRACK_WIDTH = 52;
|
||||
const TRACK_HEIGHT = 30;
|
||||
const THUMB_SIZE = 26;
|
||||
const PADDING = 2;
|
||||
|
||||
export function GlassToggle({
|
||||
value,
|
||||
onValueChange,
|
||||
disabled = false,
|
||||
style,
|
||||
}: GlassToggleProps) {
|
||||
// Shared values for animation
|
||||
const progress = useSharedValue(value ? 1 : 0);
|
||||
|
||||
// Sync shared value when prop changes
|
||||
useEffect(() => {
|
||||
progress.value = withSpring(value ? 1 : 0, {
|
||||
mass: 0.8,
|
||||
damping: 15,
|
||||
stiffness: 120,
|
||||
});
|
||||
}, [value]);
|
||||
|
||||
const handlePress = async () => {
|
||||
if (disabled) return;
|
||||
await triggerHaptic();
|
||||
onValueChange(!value);
|
||||
};
|
||||
|
||||
const thumbStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
translateX: progress.value * (TRACK_WIDTH - THUMB_SIZE - (PADDING * 2)),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const trackStyle = useAnimatedStyle(() => {
|
||||
const backgroundColor = interpolateColor(
|
||||
progress.value,
|
||||
[0, 1],
|
||||
['rgba(255, 255, 255, 0.1)', 'transparent'] // We use LinearGradient for active state
|
||||
);
|
||||
|
||||
return {
|
||||
backgroundColor,
|
||||
};
|
||||
});
|
||||
|
||||
const activeGradientStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: progress.value,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={handlePress}
|
||||
disabled={disabled}
|
||||
style={[styles.container, style, disabled && styles.disabled]}
|
||||
>
|
||||
<Animated.View style={[styles.track, trackStyle]}>
|
||||
{/* Active Gradient Background (fades in) */}
|
||||
<Animated.View style={[StyleSheet.absoluteFill, activeGradientStyle]}>
|
||||
<LinearGradient
|
||||
colors={gradients.primary as [string, string, ...string[]]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
{/* Thumb */}
|
||||
<Animated.View style={[styles.thumb, thumbStyle]} />
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: TRACK_WIDTH,
|
||||
height: TRACK_HEIGHT,
|
||||
},
|
||||
track: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: radius.full,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||
overflow: 'hidden',
|
||||
justifyContent: 'center',
|
||||
padding: PADDING,
|
||||
},
|
||||
thumb: {
|
||||
width: THUMB_SIZE,
|
||||
height: THUMB_SIZE,
|
||||
borderRadius: radius.full,
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
},
|
||||
disabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
});
|
||||
@@ -10,5 +10,6 @@ export { LoadingOverlay } from './LoadingOverlay';
|
||||
export type { LoadingOverlayProps, LoadingVariant } from './LoadingOverlay';
|
||||
export { ReadReceipt, getReceiptStatus } from './ReadReceipt';
|
||||
export type { ReadReceiptProps, ReceiptStatus } from './ReadReceipt';
|
||||
export { GlassToggle } from './GlassToggle';
|
||||
export { AmbientOrbs } from './AmbientOrbs';
|
||||
export type { AmbientOrbsProps, OrbVariant, OrbPosition } from './AmbientOrbs';
|
||||
Reference in new issue
Block a user