Enhance swipe UI and add chat date separators

This commit is contained in:
jjbrunton committed 2026-01-03 18:37:18 +00:00
1 parent 7e686c147a
commit a567fba349
12 files changed
+507 -286

No files matched your search

@@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import { View, Text, StyleSheet, Platform, useWindowDimensions } from "react-native";
import { View, Text, StyleSheet, Platform, useWindowDimensions, TouchableOpacity as RNTouchableOpacity } from "react-native";
import { Gesture, GestureDetector, TouchableOpacity } from "react-native-gesture-handler";
import { BlurView } from "expo-blur";
import { LinearGradient } from "expo-linear-gradient";
@@ -36,7 +36,7 @@ const MAX_CARD_WIDTH = 400;
const SWIPE_THRESHOLD = 100;
interface Props {
question: { id: string; text: string; intensity: number; partner_text?: string | null; is_two_part?: boolean };
question: { id: string; text: string; intensity: number; partner_text?: string | null; is_two_part?: boolean; partner_answered?: boolean };
onSwipe: (direction: "left" | "right" | "up" | "down") => void;
}
@@ -305,14 +305,18 @@ function CardContent({
/>
{/* Feedback Button */}
<View style={styles.feedbackButton}>
<RNTouchableOpacity
style={styles.feedbackButton}
onPress={onFeedbackPress}
hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
activeOpacity={0.7}
>
<Ionicons
name="flag-outline"
size={18}
size={22}
color={colors.textTertiary}
onPress={onFeedbackPress}
/>
</View>
</RNTouchableOpacity>
{/* Overlays with gradient badges */}
<Animated.View style={[styles.overlay, overlayStyle("right")]} pointerEvents="none">
@@ -368,6 +372,21 @@ function CardContent({
<Text style={[styles.text, question.is_two_part && styles.twoPartText]}>
{question.text}
</Text>
{/* Partner's version of the question - only show when user is answering first */}
{question.partner_text && !question.partner_answered && (
<View style={styles.partnerTextContainer}>
<View style={styles.partnerDivider}>
<View style={styles.partnerDividerLine} />
<View style={styles.partnerLabelContainer}>
<Ionicons name="swap-horizontal" size={12} color={colors.secondary} />
<Text style={styles.partnerLabel}>Partner sees</Text>
</View>
<View style={styles.partnerDividerLine} />
</View>
<Text style={styles.partnerText}>{question.partner_text}</Text>
</View>
)}
</View>
{/* Action Buttons */}
@@ -524,14 +543,14 @@ const styles = StyleSheet.create({
position: 'absolute',
top: spacing.md,
right: spacing.md,
width: 36,
height: 36,
borderRadius: 18,
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: colors.glass.background,
justifyContent: 'center',
alignItems: 'center',
zIndex: 100,
elevation: 10,
zIndex: 999,
elevation: 999,
borderWidth: 1,
borderColor: colors.glass.border,
},
@@ -565,6 +584,42 @@ const styles = StyleSheet.create({
...typography.title3,
lineHeight: 28,
},
partnerTextContainer: {
marginTop: spacing.lg,
width: '100%',
paddingHorizontal: spacing.sm,
},
partnerDivider: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: spacing.sm,
},
partnerDividerLine: {
flex: 1,
height: 1,
backgroundColor: colors.glass.border,
},
partnerLabelContainer: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
backgroundColor: colors.secondaryLight,
borderRadius: radius.full,
gap: spacing.xs,
},
partnerLabel: {
...typography.caption1,
color: colors.secondary,
fontWeight: '500',
letterSpacing: 0.3,
},
partnerText: {
...typography.subhead,
color: colors.textSecondary,
textAlign: 'center',
fontStyle: 'italic',
},
footer: {
paddingBottom: spacing.lg,
paddingHorizontal: spacing.lg,
@@ -7,7 +7,7 @@ const MAX_CARD_WIDTH = 400;
const SWIPE_THRESHOLD = 100;
interface Props {
question: { id: string; text: string; intensity: number; partner_text?: string | null };
question: { id: string; text: string; intensity: number; partner_text?: string | null; partner_answered?: boolean };
onSwipe: (direction: "left" | "right" | "up" | "down") => void;
}
@@ -208,6 +208,21 @@ export default function SwipeCard({ question, onSwipe }: Props) {
))}
</View>
<Text style={[styles.text, question.partner_text ? styles.twoPartText : null]}>{question.text}</Text>
{/* Partner's version of the question - only show when user is answering first */}
{question.partner_text && !question.partner_answered && (
<View style={styles.partnerTextContainer}>
<View style={styles.partnerDivider}>
<View style={styles.partnerDividerLine} />
<View style={styles.partnerLabelContainer}>
<Ionicons name="swap-horizontal" size={12} color="#9b59b6" />
<Text style={styles.partnerLabel}>Partner sees</Text>
</View>
<View style={styles.partnerDividerLine} />
</View>
<Text style={styles.partnerText}>{question.partner_text}</Text>
</View>
)}
</View>
<View style={styles.footer}>
@@ -369,26 +384,41 @@ const styles = StyleSheet.create({
paddingVertical: 8,
borderRadius: 12,
},
partnerContainer: {
partnerTextContainer: {
marginTop: 24,
width: "100%",
alignItems: "center",
paddingHorizontal: 8,
},
divider: {
partnerDivider: {
flexDirection: "row",
alignItems: "center",
marginBottom: 8,
},
partnerDividerLine: {
flex: 1,
height: 1,
width: "80%",
backgroundColor: "rgba(255,255,255,0.1)",
marginVertical: 24,
position: "relative",
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(255, 255, 255, 0.08)",
},
dividerText: {
color: "#666",
backgroundColor: "#16213e",
paddingHorizontal: 10,
partnerLabelContainer: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
paddingVertical: 4,
backgroundColor: "rgba(155, 89, 182, 0.15)",
borderRadius: 9999,
gap: 4,
},
partnerLabel: {
fontSize: 12,
fontWeight: "bold",
position: "absolute",
color: "#9b59b6",
fontWeight: "500",
letterSpacing: 0.3,
},
partnerText: {
fontSize: 14,
color: "rgba(255, 255, 255, 0.7)",
textAlign: "center",
fontStyle: "italic",
},
buttonContainer: {
flexDirection: "row",
@@ -53,11 +53,12 @@ const MAX_CARD_WIDTH = 400;
const SWIPE_THRESHOLD = 100;
interface Props {
question: { id: string; text: string; intensity: number; partner_text?: string | null; is_two_part?: boolean };
question: { id: string; text: string; intensity: number; partner_text?: string | null; is_two_part?: boolean; partner_answered?: boolean };
onSwipe: (direction: "left" | "right" | "up" | "down") => void;
onReport?: () => void;
}
export default function SwipeCardPremium({ question, onSwipe }: Props) {
export default function SwipeCardPremium({ question, onSwipe, onReport }: Props) {
const { width: screenWidth } = useWindowDimensions();
const cardWidth = Math.min(screenWidth - 48, MAX_CARD_WIDTH);
@@ -173,6 +174,17 @@ export default function SwipeCardPremium({ question, onSwipe }: Props) {
return (
<View style={[styles.cardWrapper, { width: cardWidth }]}>
{/* Report Button - outside GestureDetector to receive touches */}
{onReport && (
<Pressable
style={styles.reportButton}
onPress={onReport}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Ionicons name="flag-outline" size={16} color={colors.textTertiary} />
</Pressable>
)}
<GestureDetector gesture={gesture}>
<Animated.View style={[styles.cardOuter, animatedStyle, cardShadowStyle, { shadowColor: ACCENT }]}>
{/* Premium dark background */}
@@ -244,6 +256,21 @@ export default function SwipeCardPremium({ question, onSwipe }: Props) {
{question.text}
</Text>
{/* Partner's version of the question - only show when user is answering first */}
{question.partner_text && !question.partner_answered && (
<View style={styles.partnerTextContainer}>
<View style={styles.partnerDivider}>
<View style={styles.partnerDividerLine} />
<View style={styles.partnerLabelBadge}>
<Ionicons name="swap-horizontal" size={10} color={ROSE} />
<Text style={styles.partnerLabelText}>PARTNER SEES</Text>
</View>
<View style={styles.partnerDividerLine} />
</View>
<Text style={styles.partnerQuestionText}>{question.partner_text}</Text>
</View>
)}
{/* Bottom separator */}
<View style={styles.separator}>
<View style={styles.separatorLine} />
@@ -366,6 +393,14 @@ const styles = StyleSheet.create({
right: 0,
height: 120,
},
reportButton: {
position: 'absolute',
top: spacing.md,
right: spacing.md,
zIndex: 100,
padding: spacing.sm,
opacity: 0.6,
},
premiumBorder: {
position: 'absolute',
top: 0,
@@ -430,6 +465,45 @@ const styles = StyleSheet.create({
lineHeight: 32,
paddingHorizontal: spacing.sm,
},
partnerTextContainer: {
marginTop: spacing.lg,
width: '100%',
paddingHorizontal: spacing.sm,
},
partnerDivider: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: spacing.sm,
},
partnerDividerLine: {
flex: 1,
height: 1,
backgroundColor: rgba.rose(0.2),
},
partnerLabelBadge: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
backgroundColor: rgba.rose(0.1),
borderRadius: radius.full,
borderWidth: 1,
borderColor: rgba.rose(0.2),
gap: spacing.xs,
},
partnerLabelText: {
...typography.caption2,
fontWeight: '600',
letterSpacing: 1.5,
color: rgba.rose(0.8),
},
partnerQuestionText: {
...typography.subhead,
color: colors.textSecondary,
textAlign: 'center',
fontStyle: 'italic',
opacity: 0.85,
},
footer: {
paddingBottom: spacing.xl,
paddingHorizontal: spacing.lg,
@@ -78,24 +78,24 @@ const STEPS: TutorialStep[] = [
icon: "arrow-down",
color: colors.textTertiary,
gradient: ["#6c757d", "#495057"],
translateX: 0,
translateY: 80,
isSwipe: true,
},
{
key: "feedback",
label: "FEEDBACK",
description: "Tap the flag to report issues with a question",
icon: "flag-outline",
color: colors.primary,
gradient: gradients.primary as [string, string],
translateX: 0,
translateY: 0,
isSwipe: false,
},
];
export default function SwipeTutorial({ onComplete }: Props) {
translateX: 0,
translateY: 80,
isSwipe: true,
},
{
key: "feedback",
label: "FEEDBACK",
description: "Tap the flag to report issues with a question",
icon: "flag-outline",
color: colors.primary,
gradient: gradients.primary as [string, string],
translateX: 0,
translateY: 0,
isSwipe: false,
},
];
export default function SwipeTutorial({ onComplete }: Props) {
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
const insets = useSafeAreaInsets();
const [currentStep, setCurrentStep] = useState(0);
@@ -330,27 +330,27 @@ export default function SwipeTutorial({ onComplete }: Props) {
</View>
)}
{/* Demo Card */}
<Animated.View style={[styles.demoCard, cardAnimatedStyle, shadows.xl]}>
<LinearGradient
colors={[colors.glass.backgroundLight, colors.glass.background]}
style={styles.demoCardInner}
>
{/* Feedback button indicator */}
<View style={[
styles.feedbackButtonDemo,
!isSwipeStep && styles.feedbackButtonHighlight
]}>
{!isSwipeStep && (
<Animated.View style={[styles.feedbackPulse, pulseAnimatedStyle]}>
<View style={[styles.pulseInner, { backgroundColor: currentStep_.color }]} />
</Animated.View>
)}
<Ionicons name="flag-outline" size={18} color={!isSwipeStep ? colors.text : colors.textTertiary} />
</View>
{/* Overlay - only for swipe steps */}
{isSwipeStep && (
{/* Demo Card */}
<Animated.View style={[styles.demoCard, cardAnimatedStyle, shadows.xl]}>
<LinearGradient
colors={[colors.glass.backgroundLight, colors.glass.background]}
style={styles.demoCardInner}
>
{/* Feedback button indicator */}
<View style={[
styles.feedbackButtonDemo,
!isSwipeStep && styles.feedbackButtonHighlight
]}>
{!isSwipeStep && (
<Animated.View style={[styles.feedbackPulse, pulseAnimatedStyle]}>
<View style={[styles.pulseInner, { backgroundColor: currentStep_.color }]} />
</Animated.View>
)}
<Ionicons name="flag-outline" size={18} color={!isSwipeStep ? colors.text : colors.textTertiary} />
</View>
{/* Overlay - only for swipe steps */}
{isSwipeStep && (
<Animated.View
style={[
styles.demoOverlay,
@@ -552,40 +552,40 @@ const styles = StyleSheet.create({
borderRadius: radius.xl,
overflow: "visible",
},
demoCardInner: {
flex: 1,
borderRadius: radius.xl,
borderWidth: 1,
borderColor: colors.glass.border,
overflow: "hidden",
},
feedbackButtonDemo: {
position: 'absolute',
top: spacing.md,
right: spacing.md,
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: colors.glass.background,
justifyContent: 'center',
alignItems: 'center',
zIndex: 100,
borderWidth: 1,
borderColor: colors.glass.border,
},
feedbackButtonHighlight: {
backgroundColor: colors.primary,
borderColor: colors.primary,
},
feedbackPulse: {
position: "absolute",
width: 56,
height: 56,
borderRadius: 28,
justifyContent: "center",
alignItems: "center",
},
demoOverlay: {
demoCardInner: {
flex: 1,
borderRadius: radius.xl,
borderWidth: 1,
borderColor: colors.glass.border,
overflow: "hidden",
},
feedbackButtonDemo: {
position: 'absolute',
top: spacing.md,
right: spacing.md,
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: colors.glass.background,
justifyContent: 'center',
alignItems: 'center',
zIndex: 100,
borderWidth: 1,
borderColor: colors.glass.border,
},
feedbackButtonHighlight: {
backgroundColor: colors.primary,
borderColor: colors.primary,
},
feedbackPulse: {
position: "absolute",
width: 56,
height: 56,
borderRadius: 28,
justifyContent: "center",
alignItems: "center",
},
demoOverlay: {
...StyleSheet.absoluteFillObject,
justifyContent: "center",
alignItems: "center",