freshyo/packages/ui/src/components/dialog.tsx
2026-08-14 23:45:40 +05:30

271 lines
7 KiB
TypeScript
Executable file

import React, { ReactNode, useState } from 'react';
import { Modal, View, TouchableOpacity, StyleSheet, Animated, Easing, Dimensions, TextInput, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
import MyText from './text';
import MyButton from './button';
import tw from "../lib/tailwind";
interface DialogProps {
open: boolean;
onClose: () => void;
children: ReactNode;
enableDismiss?: boolean;
}
const SCREEN_HEIGHT = Dimensions.get('window').height;
export const BottomDialog: React.FC<DialogProps> = ({ open, onClose, children, enableDismiss = true }) => {
const [slideAnim] = useState(new Animated.Value(SCREEN_HEIGHT));
React.useEffect(() => {
if (open) {
Animated.timing(slideAnim, {
toValue: 0,
duration: 300,
easing: Easing.out(Easing.cubic),
useNativeDriver: true,
}).start();
} else {
Animated.timing(slideAnim, {
toValue: SCREEN_HEIGHT,
duration: 200,
useNativeDriver: true,
}).start();
}
}, [open]);
return (
<Modal
visible={open}
transparent
animationType="none"
onRequestClose={enableDismiss ? onClose : undefined}
>
{enableDismiss ? (
<TouchableOpacity style={styles.backdrop} activeOpacity={1} onPress={onClose} />
) : (
<View style={[styles.backdrop, styles.disabledBackdrop]} />
)}
<Animated.View
style={[
styles.dialog,
{
transform: [{ translateY: slideAnim }],
},
]}
>
<View style={styles.handle} />
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardAvoidingContainer}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
>
<ScrollView
style={styles.scrollContainer}
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={true}
>
{children}
</ScrollView>
</KeyboardAvoidingView>
</Animated.View>
</Modal>
);
};
export const RawBottomDialog: React.FC<DialogProps> = ({ open, onClose, children, enableDismiss = true }) => {
const [slideAnim] = useState(new Animated.Value(SCREEN_HEIGHT));
React.useEffect(() => {
if (open) {
Animated.timing(slideAnim, {
toValue: 0,
duration: 300,
easing: Easing.out(Easing.cubic),
useNativeDriver: true,
}).start();
} else {
Animated.timing(slideAnim, {
toValue: SCREEN_HEIGHT,
duration: 200,
useNativeDriver: true,
}).start();
}
}, [open]);
return (
<Modal
visible={open}
transparent
animationType="none"
onRequestClose={enableDismiss ? onClose : undefined}
>
{enableDismiss ? (
<TouchableOpacity style={tw`flex-1 bg-black/30`} activeOpacity={1} onPress={onClose} />
) : (
<View style={[tw`flex-1 bg-black/30`, { pointerEvents: 'none' }]} />
)}
{children}
{/* <Animated.View
style={[
tw`absolute left-0 right-0 bottom-0 bg-white rounded-t-[18px] px-5 pb-0 shadow-lg`,
{
maxHeight: SCREEN_HEIGHT * 0.85,
transform: [{ translateY: slideAnim }],
shadowColor: '#000',
shadowOffset: { width: 0, height: -2 },
shadowOpacity: 0.2,
shadowRadius: 8,
elevation: 8,
},
]}
>
{children}
</Animated.View> */}
</Modal>
);
};
const styles = StyleSheet.create({
backdrop: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.3)',
},
disabledBackdrop: {
pointerEvents: 'none',
},
dialog: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
backgroundColor: '#fff',
borderTopLeftRadius: 18,
borderTopRightRadius: 18,
paddingHorizontal: 20,
paddingBottom: 0,
elevation: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: -2 },
shadowOpacity: 0.2,
shadowRadius: 8,
maxHeight: SCREEN_HEIGHT * 0.85, // Limit max height so it doesn't cover entire screen
},
handle: {
width: 40,
height: 5,
borderRadius: 3,
backgroundColor: '#ccc',
alignSelf: 'center',
marginTop: 12,
marginBottom: 8,
},
keyboardAvoidingContainer: {
flex: 1,
width: '100%',
},
scrollContainer: {
flex: 1,
width: '100%',
},
scrollContent: {
flexGrow: 1,
paddingBottom: Platform.OS === 'ios' ? 40 : 20, // Extra padding for iOS keyboard
},
});
const commentStyles = StyleSheet.create({
container: {
marginBottom: 16,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 10,
minHeight: 80,
fontSize: 16,
},
});
export default BottomDialog;
interface ConfirmationDialogProps {
open: boolean;
positiveAction: (comment?: string) => void;
commentNeeded?: boolean;
negativeAction?: () => void;
title?: string;
message?: string;
confirmText?: string;
cancelText?: string;
isLoading?: boolean;
}
export const ConfirmationDialog: React.FC<ConfirmationDialogProps> = (props) => {
const {
open,
positiveAction,
commentNeeded = false,
negativeAction,
title = "Are you sure?",
message = "Do you really want to proceed with this action?",
confirmText = "Confirm",
cancelText = "Cancel",
isLoading = false,
} = props;
const [comment, setComment] = useState('');
const handleConfirm = () => {
positiveAction(commentNeeded ? comment : undefined);
setComment(''); // Reset comment after action
};
const handleCancel = () => {
if (negativeAction) {
negativeAction();
}
setComment(''); // Reset comment on cancel
};
return (
<BottomDialog open={open} onClose={handleCancel}>
<View style={{ padding: 20 }}>
<MyText style={{ fontSize: 18, fontWeight: 'bold', marginBottom: 16 }}>
{title}
</MyText>
<MyText style={{ marginBottom: commentNeeded ? 16 : 24 }}>
{message}
</MyText>
{commentNeeded && (
<View style={commentStyles.container}>
<TextInput
style={commentStyles.input}
value={comment}
onChangeText={setComment}
placeholder="Add a comment..."
multiline={true}
numberOfLines={3}
textAlignVertical="top"
/>
</View>
)}
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: commentNeeded ? 16 : 0 }}>
<MyButton textContent={cancelText} onPress={handleCancel} fillColor="gray1" textColor="white1" disabled={isLoading} />
<MyButton
textContent={isLoading ? "Processing..." : confirmText}
style={{ flexShrink: 0 }}
onPress={handleConfirm}
fillColor="red1"
textColor="white1"
disabled={isLoading}
/>
</View>
</View>
</BottomDialog>
);
}