Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions src/components/GradientButton.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
/*
GradientButton
- Reusable gradient button component with optional hold-to-confirm functionality
- Updated 2026-01-22: Added hold-to-confirm feature for critical actions.
When holdToConfirm={true}, user must press and hold for specified duration (default 2.5s).
Shows animated progress fill and haptic feedback. Prevents accidental taps on critical actions.
GradientButton 
- Reusable gradient button component with optional hold-to-confirm functionality.
- Updated 2026-01-22: Added hold-to-confirm support for critical actions while
keeping the default gradient button API unchanged for ordinary taps.
*/
import React, { useRef, useEffect, useState } from 'react';
import { TouchableOpacity, View, StyleSheet, Animated, Vibration, Platform } from 'react-native';
import { TouchableOpacity, View, Animated, Vibration, Platform } from 'react-native';
import { Text } from 'react-native-paper';
import Svg, { Defs, LinearGradient, Stop, Rect } from 'react-native-svg';
import styles from '../styles/gradientbutton.styles';
import { gradientButtonStyles as styles } from '../styles';

const GradientButton = ({
onPress,
Expand All @@ -18,8 +17,8 @@ const GradientButton = ({
style,
contentStyle,
labelStyle,
topColor = '#53C6F4',
bottomColor = '#30A1CE',
topColor = '#3165D4',
bottomColor = '#3165D4',
mode = 'contained',
leftIcon,
rightIcon,
Expand Down Expand Up @@ -129,7 +128,7 @@ const GradientButton = ({
style={[
styles.gradientButtonLabel,
isOutlined && {
color: bottomColor,
color: bottomColor,
textShadowColor: 'transparent',
textShadowOffset: { width: 0, height: 0 },
textShadowRadius: 0
Expand Down Expand Up @@ -223,4 +222,5 @@ const GradientButton = ({
);
};

// Keep this reusable button self-contained; integrated by Codex GPT-5 while preserving the existing call sites.
export default GradientButton;
173 changes: 136 additions & 37 deletions src/components/ListSelectionModal/ListSelectionModal.js
Original file line number Diff line number Diff line change
@@ -1,64 +1,163 @@
/*
This component displays a modal with a text input to enter number values
ListSelectionModal
- Modal with searchable list selection
- Updated 2026-01-22: Converted to modern SemiModal with search bar
*/

import React, { Component } from "react";
import SemiModal from "../SemiModal";
import { List, Text } from "react-native-paper"
import { TouchableOpacity, FlatList, View } from "react-native"
import Styles from "../../styles";
import { List } from "react-native-paper"
import { TouchableOpacity, FlatList, View, TextInput as RNTextInput, KeyboardAvoidingView, Platform } from "react-native"
import { listSelectionModalStyles as styles } from "../../styles";
import Colors from "../../globals/colors";
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';

class ListSelectionModal extends Component {
constructor(props) {
super(props);
this.state = {
searchQuery: '',
searchFocused: false,
};
}

componentDidUpdate(prevProps) {
if (
prevProps.visible &&
!this.props.visible &&
(this.state.searchQuery.length > 0 || this.state.searchFocused)
) {
this.setState({ searchQuery: '', searchFocused: false });
}
}

handleSelect = (item) => {
const { onSelect, cancel } = this.props;
this.setState({ searchQuery: '' });
if (onSelect) {
onSelect(item);
cancel();
}
}

getFilteredData = () => {
const { data, showSearch } = this.props;
const { searchQuery } = this.state;
const dataList = Array.isArray(data) ? data : [];

if (!showSearch || !searchQuery.trim()) {
return dataList;
}

const query = searchQuery.toLowerCase().trim();
return dataList.filter((item) =>
item.title?.toLowerCase().includes(query) ||
item.description?.toLowerCase().includes(query) ||
item.key?.toString().toLowerCase().includes(query)
);
}

render() {
const { visible, cancel, selectedKey, onSelect, data, title, flexHeight } = this.props;
const {
visible,
cancel,
selectedKey,
title,
flexHeight,
showSearch = false,
searchPlaceholder = "Search",
keyExtractor,
} = this.props;
const { searchQuery, searchFocused } = this.state;
const filteredData = this.getFilteredData();

return (
<SemiModal
animationType="slide"
transparent={true}
visible={visible}
onRequestClose={cancel}
flexHeight={flexHeight ? flexHeight : 3}
title={title}
flexHeight={flexHeight ? flexHeight : 0.01}
contentContainerStyle={{
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
flex: 0,
width: '100%',
alignSelf: 'flex-end',
maxHeight: '80%',
}}
>
<View style={Styles.centerContainer}>
<View style={{ ...Styles.headerContainerSafeArea, minHeight: 36, maxHeight: 36, paddingBottom: 8 }}>
<Text
style={{
...Styles.centralHeader,
...Styles.smallMediumFont
}}
>
{title}
</Text>
</View>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={styles.container}
keyboardVerticalOffset={Platform.OS === "ios" ? 40 : 0}
>
<View style={styles.container}>
{showSearch && (
<View style={styles.searchContainer}>
<View
style={[
styles.searchInputContainer,
searchFocused && styles.searchInputFocused,
]}
>
<RNTextInput
value={searchQuery}
onChangeText={(text) => this.setState({ searchQuery: text })}
onFocus={() => this.setState({ searchFocused: true })}
onBlur={() => this.setState({ searchFocused: false })}
placeholder={searchPlaceholder}
placeholderTextColor="#999"
autoCorrect={false}
autoCapitalize="none"
returnKeyType="search"
style={styles.searchInput}
/>
<View style={styles.searchIcon}>
<MaterialCommunityIcons name="magnify" size={20} color="#999" />
</View>
</View>
</View>
)}

{/* Currency list */}
<FlatList
style={Styles.fullWidth}
style={styles.listContainer}
contentContainerStyle={styles.listContent}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
renderItem={({ item }) => {
return (
<TouchableOpacity onPress={onSelect != null ? () => {
onSelect(item)
cancel()
} : undefined}>
<List.Item
title={item.title}
description={item.description}
right={
item.key === selectedKey
? (props) => <List.Icon {...props} icon={"check"} />
: (props) => <List.Icon {...props} color={Colors.secondaryColor} icon={"check"} />
}
/>
</TouchableOpacity>
);
}}
data={data}
const isSelected = item.key === selectedKey;
return (
<TouchableOpacity
onPress={() => this.handleSelect(item)}
activeOpacity={0.7}
>
<List.Item
title={item.title}
description={item.description}
titleStyle={styles.itemTitle}
descriptionStyle={styles.itemDescription}
right={(props) => (
<List.Icon
{...props}
icon="check"
color={isSelected ? Colors.primaryColor : 'transparent'}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You deleted the import for colors but colors is here, this will cause a crash

/>
)}
style={styles.listItem}
/>
</TouchableOpacity>
);
}}
data={filteredData}
keyExtractor={keyExtractor || ((item, index) => (
item?.key != null ? item.key.toString() : `item-${index}`
))}
/>
</View>
</View>
</KeyboardAvoidingView>
</SemiModal>
);
}
Expand Down
104 changes: 97 additions & 7 deletions src/components/SemiModal.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
/*
This component creates a modal that covers half of
the screen, while darkening the content behind it
SemiModal
- 2026-01-12: Added an optional standardized sheet header (centered title + top-right X
in a light grey circle) to replace ad-hoc blue "Close" text buttons across sheets.
Header is opt-in via `title`/`showHeader` to avoid breaking existing custom layouts.
*/

import React, { Component } from "react";
import {
View,
Text,
Animated,
TouchableWithoutFeedback
TouchableWithoutFeedback,
TouchableOpacity
} from "react-native";
import Colors from "../globals/colors";
import Styles from "../styles/index";
import Modal from './Modal'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';

class SemiModal extends Component {
constructor(props) {
Expand Down Expand Up @@ -48,6 +53,18 @@ class SemiModal extends Component {

render() {
const flexHeight = this.props.flexHeight != null ? this.props.flexHeight : 1
const showHeader = this.props.showHeader != null
? this.props.showHeader
: this.props.title != null;

const closeDisabled = !!this.props.closeDisabled;
const onRequestClose = this.props.onRequestClose;
const effectiveOnRequestClose = closeDisabled
? () => {}
: onRequestClose;

// Header layout uses a fixed-size right action so the title stays centered.
const closeButtonSize = 34;

return (
<React.Fragment>
Expand All @@ -65,23 +82,96 @@ class SemiModal extends Component {
animationType={this.props.animationType}
transparent={true}
visible={this.props.visible}
onRequestClose={this.props.onRequestClose}
onDismiss={this.props.onRequestClose}
onRequestClose={effectiveOnRequestClose}
onDismiss={effectiveOnRequestClose}
>
<TouchableWithoutFeedback onPress={this.props.onRequestClose}>
<TouchableWithoutFeedback onPress={effectiveOnRequestClose}>
<View style={{ flex: 1 }} />
</TouchableWithoutFeedback>
<View
style={{
flex: flexHeight,
backgroundColor: Colors.secondaryColor,
borderRadius: 10,
paddingTop: 10,
paddingTop: showHeader ? 0 : 10,
...(this.props.contentContainerStyle
? this.props.contentContainerStyle
: {}),
}}
>
{showHeader && (
<View
style={{
paddingHorizontal: 12,
paddingTop: 12,
paddingBottom: 12,
}}
>
{/* Centered title (absolute) */}
<View
pointerEvents="none"
style={{
position: 'absolute',
left: 12,
right: 12,
top: 12,
bottom: 12,
alignItems: 'center',
justifyContent: 'center',
}}
>
{typeof this.props.title === 'string' ? (
<Text
style={{
fontSize: 16,
fontWeight: '600',
color: '#1A1A1A',
}}
numberOfLines={1}
>
{this.props.title}
</Text>
) : (
this.props.title
)}
</View>

{/* Left + Right actions */}
<View
style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<View style={{ minWidth: closeButtonSize, minHeight: closeButtonSize }}>
{this.props.headerLeft != null ? this.props.headerLeft : null}
</View>
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={this.props.closeAccessibilityLabel || 'Close'}
onPress={closeDisabled ? undefined : onRequestClose}
activeOpacity={0.7}
disabled={closeDisabled}
style={{
width: closeButtonSize,
height: closeButtonSize,
borderRadius: closeButtonSize / 2,
backgroundColor: '#EEF0F3',
alignItems: 'center',
justifyContent: 'center',
opacity: closeDisabled ? 0.5 : 1,
}}
>
<MaterialCommunityIcons
name="close"
size={18}
color="#111"
/>
</TouchableOpacity>
</View>
</View>
)}
{this.props.children}
</View>
</Modal>
Expand Down
Loading