mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-01 18:19:43 +00:00
Enhance swipe UI and add chat date separators
This commit is contained in:
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",
|
||||
|
||||
Reference in new issue
Block a user