refactor(bottom-sheets): polish edit sheets (buttons, keyboard, layout)

EditNameBottomSheet :
- Render conditionnel (mounted seulement quand editingName=true) pour ne plus
  ouvrir le clavier auto à l'arrivée sur ScanDetail
- BottomSheetScrollView avec contentContainerStyle inline (Tailwind pour le
  reste) + insets.bottom pour padding bas
- isDirty disable du Save quand le nom n'a pas changé
- Boutons Annuler/Enregistrer en row via inner View avec icônes X/Check,
  font-bold, minHeight 56px

EditProfileModal :
- BottomSheetScrollView : actions déplacées DANS le scroll (juste après le
  dernier input email) → toujours visibles sous le formulaire, jamais
  poussées en bas du sheet
- snap 95% + topInset safe-area
- Boutons même style (icônes + ghost grisé bordé + primary shadow)

MapBottomSheet rename inline :
- useImperativeHandle pour forwarder le ref correctement et avoir un
  internalRef accessible côté composant
- snapToIndex(2) à l'ouverture du form (85%) puis snapToIndex(0) après
  save/cancel pour redonner la map
- BottomSheetScrollView pour le rename form avec keyboardShouldPersistTaps
- keyboardBehavior 'interactive' + android_keyboardInputMode 'adjustResize'
- containerStyle { zIndex:100, elevation:100 } pour passer au-dessus des
  FloatingActions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yanis 2026-05-01 00:36:44 +02:00
parent 28d4fc176e
commit 07f25769e3
3 changed files with 453 additions and 723 deletions

View file

@ -6,12 +6,7 @@ import {
useRef,
useState,
} from "react";
import {
View,
Pressable,
StyleSheet,
Text as RNText,
} from "react-native";
import { View, Pressable } from "react-native";
import BottomSheet, {
BottomSheetScrollView,
BottomSheetTextInput,
@ -86,38 +81,50 @@ export const MapBottomSheet = forwardRef<BottomSheet, MapBottomSheetProps>(
}
}, [renamingScan, t]);
const trimmedDraft = draftName.trim();
const initialDraft = renamingScan
? getScanDisplayName(renamingScan, t).trim()
: "";
const isDraftDirty =
trimmedDraft.length > 0 && trimmedDraft !== initialDraft;
function handleStartRename(scan: ScanRecord) {
setRenamingScan(scan);
// remonte à 85% pour bien voir l'input + boutons au-dessus du clavier
internalRef.current?.snapToIndex(2);
}
function handleConfirmRename() {
if (!isDraftDirty) return;
if (renamingScan) {
onRename?.(renamingScan.id, draftName);
onRename?.(renamingScan.id, trimmedDraft);
}
setRenamingScan(null);
setDraftName("");
// redescend pour voir la map
internalRef.current?.snapToIndex(0);
}
function handleCancelRename() {
setRenamingScan(null);
setDraftName("");
// redescend pour voir la map
internalRef.current?.snapToIndex(0);
}
return (
<>
<BottomSheet
ref={internalRef}
index={defaultIndex}
snapPoints={snapPoints}
handleIndicatorStyle={styles.handleIndicator}
backgroundStyle={styles.background}
containerStyle={styles.sheetContainer}
handleIndicatorStyle={{
backgroundColor: colors.neutral[300],
width: 40,
height: 4,
}}
backgroundStyle={{
backgroundColor: "#FFFFFF",
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
}}
containerStyle={{ zIndex: 100, elevation: 100 }}
enableDynamicSizing={false}
enablePanDownToClose={false}
keyboardBehavior="interactive"
@ -126,25 +133,29 @@ export const MapBottomSheet = forwardRef<BottomSheet, MapBottomSheetProps>(
>
{renamingScan ? (
<BottomSheetScrollView
contentContainerStyle={[
styles.renameWrap,
{ paddingBottom: 24 },
]}
contentContainerStyle={{
paddingHorizontal: 20,
paddingTop: 4,
paddingBottom: 24,
gap: 12,
}}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<View style={styles.renameHeader}>
<View className="flex-row items-center justify-between">
<Pressable
onPress={handleCancelRename}
hitSlop={10}
style={styles.backBtn}
className="w-9 h-9 rounded-full items-center justify-center bg-[#FAFAFA]"
>
<ChevronLeft size={20} color={colors.neutral[700]} />
</Pressable>
<Text style={styles.title}>{t("map.rename.title")}</Text>
<View style={styles.backBtnPlaceholder} />
<Text className="text-lg font-bold text-[#1B1B1B]">
{t("map.rename.title")}
</Text>
<View className="w-9 h-9" />
</View>
<Text style={styles.renameSubtitle}>
<Text className="text-[13px] leading-[18px] text-[#6B6B6B] px-1">
{t("map.rename.subtitle")}
</Text>
<BottomSheetTextInput
@ -156,101 +167,122 @@ export const MapBottomSheet = forwardRef<BottomSheet, MapBottomSheetProps>(
maxLength={64}
returnKeyType="done"
onSubmitEditing={handleConfirmRename}
style={styles.renameInput}
style={{
borderWidth: 1,
borderColor: colors.neutral[300],
borderRadius: 14,
paddingHorizontal: 14,
paddingVertical: 14,
fontSize: 16,
color: colors.neutral[900],
backgroundColor: "#FAFAFA",
}}
/>
<View style={styles.renameActions}>
<View className="flex-row pt-4 gap-3">
<Pressable
onPress={handleCancelRename}
style={({ pressed }) => [
styles.renameBtn,
styles.renameBtnGhost,
pressed && { opacity: 0.7 },
]}
className="flex-1 min-h-[56px] rounded-[14px] py-4 px-3 items-center justify-center bg-[#F5F5F5] border-[1.5px] border-[#BDBDBD] active:opacity-70"
>
<View style={styles.renameBtnInner}>
<X
size={18}
color={colors.neutral[800]}
strokeWidth={2.4}
/>
<RNText style={styles.renameBtnGhostLabel}>
<View className="flex-row items-center">
<X size={18} color={colors.neutral[800]} strokeWidth={2.4} />
<Text className="text-base font-bold text-[#2D2D2D] ml-2 tracking-[0.2px]">
{t("common.cancel")}
</RNText>
</Text>
</View>
</Pressable>
<Pressable
onPress={handleConfirmRename}
style={({ pressed }) => [
styles.renameBtn,
styles.renameBtnPrimary,
pressed && { opacity: 0.85 },
]}
disabled={!isDraftDirty}
className={
isDraftDirty
? "flex-1 min-h-[56px] rounded-[14px] py-4 px-3 items-center justify-center bg-primary active:opacity-85"
: "flex-1 min-h-[56px] rounded-[14px] py-4 px-3 items-center justify-center bg-primary/40"
}
>
<View style={styles.renameBtnInner}>
<Check size={18} color="#FFFFFF" strokeWidth={2.6} />
<RNText style={styles.renameBtnPrimaryLabel}>
<View className="flex-row items-center">
<Check
size={18}
color="#FFFFFF"
strokeWidth={2.6}
opacity={isDraftDirty ? 1 : 0.7}
/>
<Text className="text-base font-bold text-white ml-2 tracking-[0.2px]">
{t("map.rename.save")}
</RNText>
</Text>
</View>
</Pressable>
</View>
</BottomSheetScrollView>
) : previewScan ? (
<View>
<View style={styles.previewHeader}>
<Text style={styles.title}>{t("map.preview.title")}</Text>
<View className="flex-row items-center justify-between px-5 pt-2 pb-1">
<Text className="text-lg font-bold text-[#1B1B1B]">
{t("map.preview.title")}
</Text>
<Pressable
onPress={onPreviewClose}
hitSlop={10}
style={styles.previewCloseBtn}
className="w-8 h-8 rounded-full items-center justify-center bg-[#FAFAFA]"
>
<X size={18} color={colors.neutral[600]} />
</Pressable>
</View>
<View style={styles.previewBody}>
<View className="px-5 pb-5 gap-2">
<ScanRow
scan={previewScan}
isLast
onPress={() => onScanPress?.(previewScan)}
onEdit={() => handleStartRename(previewScan)}
/>
<Text style={styles.previewHint}>
<Text className="text-xs font-medium text-[#9E9E9E] text-center pt-1">
{t("map.preview.tapHint")}
</Text>
</View>
</View>
) : (
<>
<View style={styles.header}>
<Text style={styles.title}>{t("map.scannedPlants")}</Text>
<Text style={styles.count}>
<View className="flex-row items-baseline justify-between px-5 pt-2 pb-3">
<Text className="text-lg font-bold text-[#1B1B1B]">
{t("map.scannedPlants")}
</Text>
<Text className="text-[13px] font-medium text-[#9E9E9E]">
{t("map.plantCount", { count: scans.length })}
</Text>
</View>
{scans.length === 0 ? (
<View style={styles.emptyState}>
<View style={styles.emptyIconWrap}>
<MapPin size={32} color={colors.primary[800]} strokeWidth={2} />
<View className="items-center px-8 py-6 gap-2">
<View className="w-16 h-16 rounded-2xl bg-[#E9F5EC] items-center justify-center mb-2">
<MapPin
size={32}
color={colors.primary[800]}
strokeWidth={2}
/>
</View>
<Text style={styles.emptyTitle}>{t("map.empty.title")}</Text>
<Text style={styles.emptySubtitle}>{t("map.empty.subtitle")}</Text>
<Text className="text-base font-bold text-[#1B1B1B] text-center">
{t("map.empty.title")}
</Text>
<Text className="text-[13px] text-[#6B6B6B] text-center leading-[18px] mb-1">
{t("map.empty.subtitle")}
</Text>
{onScanCta && (
<Pressable
onPress={onScanCta}
style={({ pressed }) => [
styles.emptyCta,
pressed && { opacity: 0.85 },
]}
className="flex-row items-center gap-2 bg-primary px-5 py-3 rounded-full mt-2 active:opacity-85"
>
<ScanLine size={18} color="#FFFFFF" strokeWidth={2.2} />
<Text style={styles.emptyCtaLabel}>{t("map.empty.cta")}</Text>
<Text className="text-white text-sm font-semibold">
{t("map.empty.cta")}
</Text>
</Pressable>
)}
</View>
) : (
<BottomSheetScrollView
contentContainerStyle={styles.listContent}
contentContainerStyle={{
paddingHorizontal: 20,
paddingBottom: 24,
}}
showsVerticalScrollIndicator={false}
>
{scans.map((scan, index) => (
@ -267,9 +299,8 @@ export const MapBottomSheet = forwardRef<BottomSheet, MapBottomSheetProps>(
</>
)}
</BottomSheet>
</>
);
}
},
);
const STATUS_TINT: Record<ScanStatus, { bg: string; fg: string }> = {
@ -301,23 +332,40 @@ function ScanRow({ scan, isLast, onPress, onEdit }: ScanRowProps) {
});
return (
<View style={[styles.row, !isLast && styles.rowBorder]}>
<Pressable onPress={onPress} style={styles.rowMain}>
<View style={[styles.iconBadge, { backgroundColor: tint.bg }]}>
<View
className={`flex-row items-center gap-2 py-3.5 ${
!isLast ? "border-b border-[#F5F5F5]" : ""
}`}
>
<Pressable
onPress={onPress}
className="flex-1 flex-row items-center gap-3.5"
>
<View
className="w-12 h-12 rounded-2xl items-center justify-center"
style={{ backgroundColor: tint.bg }}
>
<Icon size={22} color={tint.fg} strokeWidth={2.2} />
</View>
<View style={styles.rowText}>
<Text style={styles.location} numberOfLines={1}>
<View className="flex-1 gap-0.5">
<Text
className="text-[15px] font-bold text-[#1B1B1B]"
numberOfLines={1}
>
{displayName}
</Text>
<Text style={styles.region} numberOfLines={1}>
<Text className="text-[13px] text-[#6B6B6B]" numberOfLines={1}>
{formattedDate}
</Text>
</View>
</Pressable>
<Pressable onPress={onEdit} hitSlop={8} style={styles.editPencil}>
<Pressable
onPress={onEdit}
hitSlop={8}
className="w-8 h-8 rounded-full items-center justify-center bg-[#FAFAFA]"
>
<Pencil size={16} color={colors.neutral[600]} strokeWidth={2} />
</Pressable>
<Pressable onPress={onPress} hitSlop={8}>
@ -326,249 +374,3 @@ function ScanRow({ scan, isLast, onPress, onEdit }: ScanRowProps) {
</View>
);
}
const styles = StyleSheet.create({
sheetContainer: {
zIndex: 100,
elevation: 100,
},
background: {
backgroundColor: "#FFFFFF",
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
shadowColor: "#000",
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.08,
shadowRadius: 12,
elevation: 12,
},
handleIndicator: {
backgroundColor: colors.neutral[300],
width: 40,
height: 4,
},
header: {
flexDirection: "row",
alignItems: "baseline",
justifyContent: "space-between",
paddingHorizontal: 20,
paddingTop: 8,
paddingBottom: 12,
},
previewHeader: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 20,
paddingTop: 8,
paddingBottom: 4,
},
previewCloseBtn: {
width: 32,
height: 32,
borderRadius: 999,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.neutral[100],
},
previewBody: {
paddingHorizontal: 20,
paddingBottom: 20,
gap: 8,
},
previewHint: {
fontSize: 12,
fontWeight: "500",
color: colors.neutral[500],
textAlign: "center",
paddingTop: 4,
},
title: {
fontSize: 18,
fontWeight: "700",
color: colors.neutral[900],
},
count: {
fontSize: 13,
fontWeight: "500",
color: colors.neutral[500],
},
listContent: {
paddingHorizontal: 20,
paddingBottom: 24,
},
row: {
flexDirection: "row",
alignItems: "center",
gap: 8,
paddingVertical: 14,
},
rowBorder: {
borderBottomWidth: 1,
borderBottomColor: colors.neutral[200],
},
rowMain: {
flex: 1,
flexDirection: "row",
alignItems: "center",
gap: 14,
},
iconBadge: {
width: 48,
height: 48,
borderRadius: 14,
alignItems: "center",
justifyContent: "center",
},
rowText: {
flex: 1,
gap: 2,
},
location: {
fontSize: 15,
fontWeight: "700",
color: colors.neutral[900],
},
region: {
fontSize: 13,
color: colors.neutral[600],
},
editPencil: {
width: 32,
height: 32,
borderRadius: 999,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.neutral[100],
},
emptyState: {
alignItems: "center",
paddingHorizontal: 32,
paddingVertical: 24,
gap: 8,
},
emptyIconWrap: {
width: 64,
height: 64,
borderRadius: 20,
backgroundColor: colors.primary[100],
alignItems: "center",
justifyContent: "center",
marginBottom: 8,
},
emptyTitle: {
fontSize: 16,
fontWeight: "700",
color: colors.neutral[900],
textAlign: "center",
},
emptySubtitle: {
fontSize: 13,
color: colors.neutral[600],
textAlign: "center",
lineHeight: 18,
marginBottom: 4,
},
emptyCta: {
flexDirection: "row",
alignItems: "center",
gap: 8,
backgroundColor: colors.primary[800],
paddingHorizontal: 22,
paddingVertical: 12,
borderRadius: 999,
marginTop: 8,
},
emptyCtaLabel: {
color: "#FFFFFF",
fontSize: 14,
fontWeight: "600",
},
// Rename inline form
renameWrap: {
paddingHorizontal: 20,
paddingTop: 4,
paddingBottom: 24,
gap: 12,
},
renameHeader: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
},
backBtn: {
width: 36,
height: 36,
borderRadius: 999,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.neutral[100],
},
backBtnPlaceholder: {
width: 36,
height: 36,
},
renameSubtitle: {
fontSize: 13,
color: colors.neutral[600],
lineHeight: 18,
paddingHorizontal: 4,
},
renameInput: {
borderWidth: 1,
borderColor: colors.neutral[300],
borderRadius: 14,
paddingHorizontal: 14,
paddingVertical: 14,
fontSize: 16,
color: colors.neutral[900],
backgroundColor: "#FAFAFA",
},
renameActions: {
flexDirection: "row",
paddingTop: 16,
gap: 12,
},
renameBtn: {
flexGrow: 1,
flexShrink: 1,
flexBasis: 0,
paddingVertical: 16,
paddingHorizontal: 12,
borderRadius: 14,
minHeight: 56,
alignItems: "center",
justifyContent: "center",
},
renameBtnInner: {
flexDirection: "row",
alignItems: "center",
},
renameBtnGhost: {
backgroundColor: colors.neutral[200],
borderWidth: 1.5,
borderColor: colors.neutral[400],
},
renameBtnGhostLabel: {
fontSize: 16,
fontWeight: "700",
color: colors.neutral[800],
letterSpacing: 0.2,
marginLeft: 8,
},
renameBtnPrimary: {
backgroundColor: colors.primary[800],
shadowColor: colors.primary[900],
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.25,
shadowRadius: 8,
elevation: 4,
},
renameBtnPrimaryLabel: {
fontSize: 16,
fontWeight: "700",
color: "#FFFFFF",
letterSpacing: 0.2,
marginLeft: 8,
},
});

View file

@ -3,11 +3,10 @@ import {
View,
Pressable,
Modal,
KeyboardAvoidingView,
Platform,
TextInput,
TouchableWithoutFeedback,
Keyboard,
Animated,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useTranslation } from "react-i18next";
@ -30,11 +29,25 @@ export function EditNameBottomSheet({
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const [name, setName] = useState(initialName);
const [keyboardHeight, setKeyboardHeight] = useState(0);
useEffect(() => {
setName(initialName);
}, [initialName]);
useEffect(() => {
const showSub = Keyboard.addListener("keyboardDidShow", (e) => {
setKeyboardHeight(e.endCoordinates.height);
});
const hideSub = Keyboard.addListener("keyboardDidHide", () => {
setKeyboardHeight(0);
});
return () => {
showSub.remove();
hideSub.remove();
};
}, []);
const trimmedName = name.trim();
const isDirty =
trimmedName.length > 0 && trimmedName !== initialName.trim();
@ -44,26 +57,27 @@ export function EditNameBottomSheet({
onSave(trimmedName);
}
// On Android edge-to-edge, the keyboard sits on top of content. We add
// its height as bottom padding so the buttons stay visible above it.
const bottomPadding =
keyboardHeight > 0 ? keyboardHeight + 75 : insets.bottom + 34;
return (
<Modal
visible
transparent
animationType="slide"
animationType="fade"
onRequestClose={onClose}
statusBarTranslucent
>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
className="flex-1 justify-end"
>
<View className="flex-1 justify-end">
<TouchableWithoutFeedback onPress={onClose}>
<View className="absolute inset-0 bg-black/40" />
</TouchableWithoutFeedback>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View
<Animated.View
className="bg-white rounded-t-[28px] px-5 pt-3"
style={{ paddingBottom: insets.bottom + 24 }}
style={{ paddingBottom: bottomPadding }}
>
<View className="items-center pb-2">
<View className="w-10 h-1 rounded-full bg-[#E0E0E0]" />
@ -132,9 +146,8 @@ export function EditNameBottomSheet({
</View>
</Pressable>
</View>
</Animated.View>
</View>
</TouchableWithoutFeedback>
</KeyboardAvoidingView>
</Modal>
);
}

View file

@ -1,10 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import {
View,
Pressable,
StyleSheet,
Text as RNText,
} from 'react-native';
import { View, Pressable } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import BottomSheet, {
BottomSheetScrollView,
@ -56,9 +51,15 @@ export function EditProfileModal({
}
}, [visible, initialProfile]);
function handleSave() {
const trimmedName = displayName.trim();
const trimmedEmail = email.trim();
const isDirty =
trimmedName !== initialProfile.displayName.trim() ||
trimmedEmail !== initialProfile.email.trim() ||
avatar !== initialProfile.avatar;
function handleSave() {
if (!isDirty) return;
if (trimmedEmail.length > 0 && !isValidEmail(trimmedEmail)) {
toast.error(t('profile.invalidEmail'));
@ -86,58 +87,94 @@ export function EditProfileModal({
keyboardBlurBehavior="restore"
android_keyboardInputMode="adjustResize"
onClose={onClose}
backgroundStyle={styles.background}
handleIndicatorStyle={styles.handle}
containerStyle={styles.container}
backgroundStyle={{
backgroundColor: '#FFFFFF',
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
}}
handleIndicatorStyle={{
backgroundColor: colors.neutral[300],
width: 40,
height: 4,
}}
containerStyle={{ zIndex: 100, elevation: 100 }}
>
<BottomSheetScrollView
contentContainerStyle={[
styles.content,
{ paddingBottom: insets.bottom + 24 },
]}
contentContainerStyle={{
paddingHorizontal: 20,
paddingTop: 4,
paddingBottom: insets.bottom + 24,
gap: 12,
}}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<View style={styles.header}>
<Text style={styles.title}>{t('profile.editTitle')}</Text>
<Pressable onPress={onClose} hitSlop={10} style={styles.closeBtn}>
<View className="flex-row items-center justify-between">
<Text className="text-lg font-bold text-[#1B1B1B]">
{t('profile.editTitle')}
</Text>
<Pressable
onPress={onClose}
hitSlop={10}
className="w-8 h-8 rounded-full items-center justify-center bg-[#FAFAFA]"
>
<X size={18} color={colors.neutral[600]} />
</Pressable>
</View>
<Text style={styles.fieldLabel}>{t('profile.avatarLabel')}</Text>
<View style={styles.avatarRow}>
<Text className="text-xs font-semibold text-[#9E9E9E] uppercase tracking-[1px] mt-3 mb-2">
{t('profile.avatarLabel')}
</Text>
<View className="flex-row flex-wrap gap-2.5">
{AVATAR_OPTIONS.map((option) => {
const selected = option === avatar;
return (
<Pressable
key={option}
onPress={() => setAvatar(option)}
style={[
styles.avatarOption,
selected && styles.avatarOptionSelected,
]}
className={`w-[52px] h-[52px] rounded-full items-center justify-center border-2 ${
selected
? 'bg-[#E9F5EC] border-primary'
: 'bg-[#F8F9FA] border-transparent'
}`}
>
<Text style={styles.avatarEmoji}>{option}</Text>
<Text
className="text-[28px] text-center"
style={{ lineHeight: 36, includeFontPadding: false }}
>
{option}
</Text>
</Pressable>
);
})}
</View>
<Text style={styles.fieldLabel}>{t('profile.nameField')}</Text>
<Text className="text-xs font-semibold text-[#9E9E9E] uppercase tracking-[1px] mt-3 mb-2">
{t('profile.nameField')}
</Text>
<BottomSheetTextInput
style={styles.input}
value={displayName}
onChangeText={setDisplayName}
placeholder={t('profile.namePlaceholder')}
placeholderTextColor={colors.neutral[400]}
maxLength={64}
returnKeyType="next"
style={{
borderWidth: 1,
borderColor: colors.neutral[300],
borderRadius: 14,
paddingHorizontal: 14,
paddingVertical: 14,
fontSize: 16,
color: colors.neutral[900],
backgroundColor: '#FAFAFA',
}}
/>
<Text style={styles.fieldLabel}>{t('profile.emailField')}</Text>
<Text className="text-xs font-semibold text-[#9E9E9E] uppercase tracking-[1px] mt-3 mb-2">
{t('profile.emailField')}
</Text>
<BottomSheetTextInput
style={styles.input}
value={email}
onChangeText={setEmail}
placeholder={t('profile.emailPlaceholder')}
@ -148,124 +185,7 @@ export function EditProfileModal({
maxLength={128}
returnKeyType="done"
onSubmitEditing={handleSave}
/>
<View style={styles.actions}>
<Pressable
onPress={onClose}
style={({ pressed }) => [
styles.button,
styles.buttonGhost,
pressed && { opacity: 0.7 },
]}
>
<View style={styles.buttonInner}>
<X size={18} color={colors.neutral[800]} strokeWidth={2.4} />
<RNText style={styles.buttonGhostLabel}>
{t('common.cancel')}
</RNText>
</View>
</Pressable>
<Pressable
onPress={handleSave}
style={({ pressed }) => [
styles.button,
styles.buttonPrimary,
pressed && { opacity: 0.85 },
]}
>
<View style={styles.buttonInner}>
<Check size={18} color="#FFFFFF" strokeWidth={2.6} />
<RNText style={styles.buttonPrimaryLabel}>
{t('profile.saveButton')}
</RNText>
</View>
</Pressable>
</View>
</BottomSheetScrollView>
</BottomSheet>
);
}
const styles = StyleSheet.create({
container: {
zIndex: 100,
elevation: 100,
},
background: {
backgroundColor: '#FFFFFF',
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
shadowColor: '#000',
shadowOffset: { width: 0, height: -8 },
shadowOpacity: 0.18,
shadowRadius: 24,
elevation: 24,
},
handle: {
backgroundColor: colors.neutral[300],
width: 40,
height: 4,
},
content: {
paddingHorizontal: 20,
paddingTop: 4,
paddingBottom: 20,
gap: 12,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
title: {
fontSize: 18,
fontWeight: '700',
color: colors.neutral[900],
},
closeBtn: {
width: 32,
height: 32,
borderRadius: 999,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.neutral[100],
},
fieldLabel: {
fontSize: 12,
fontWeight: '600',
color: colors.neutral[500],
textTransform: 'uppercase',
letterSpacing: 1,
marginTop: 12,
marginBottom: 8,
},
avatarRow: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 10,
},
avatarOption: {
width: 52,
height: 52,
borderRadius: 999,
backgroundColor: '#F8F9FA',
alignItems: 'center',
justifyContent: 'center',
borderWidth: 2,
borderColor: 'transparent',
},
avatarOptionSelected: {
backgroundColor: colors.primary[100],
borderColor: colors.primary[800],
},
avatarEmoji: {
fontSize: 28,
lineHeight: 36,
textAlign: 'center',
includeFontPadding: false,
},
input: {
style={{
borderWidth: 1,
borderColor: colors.neutral[300],
borderRadius: 14,
@ -274,49 +194,44 @@ const styles = StyleSheet.create({
fontSize: 16,
color: colors.neutral[900],
backgroundColor: '#FAFAFA',
},
actions: {
flexDirection: 'row',
marginTop: 20,
},
button: {
flex: 1,
paddingVertical: 16,
borderRadius: 14,
minHeight: 56,
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: 6,
},
buttonInner: {
flexDirection: 'row',
alignItems: 'center',
},
buttonGhost: {
backgroundColor: colors.neutral[200],
borderWidth: 1.5,
borderColor: colors.neutral[400],
},
buttonGhostLabel: {
fontSize: 16,
fontWeight: '700',
color: colors.neutral[800],
letterSpacing: 0.2,
marginLeft: 8,
},
buttonPrimary: {
backgroundColor: colors.primary[800],
shadowColor: colors.primary[900],
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.25,
shadowRadius: 8,
elevation: 4,
},
buttonPrimaryLabel: {
fontSize: 16,
fontWeight: '700',
color: '#FFFFFF',
letterSpacing: 0.2,
marginLeft: 8,
},
});
}}
/>
<View className="flex-row pt-5 gap-3">
<Pressable
onPress={onClose}
className="flex-1 min-h-[56px] rounded-[14px] py-4 px-3 items-center justify-center bg-[#F5F5F5] border-[1.5px] border-[#BDBDBD] active:opacity-70"
>
<View className="flex-row items-center">
<X size={18} color={colors.neutral[800]} strokeWidth={2.4} />
<Text className="text-base font-bold text-[#2D2D2D] ml-2 tracking-[0.2px]">
{t('common.cancel')}
</Text>
</View>
</Pressable>
<Pressable
onPress={handleSave}
disabled={!isDirty}
className={
isDirty
? 'flex-1 min-h-[56px] rounded-[14px] py-4 px-3 items-center justify-center bg-primary active:opacity-85'
: 'flex-1 min-h-[56px] rounded-[14px] py-4 px-3 items-center justify-center bg-primary/40'
}
>
<View className="flex-row items-center">
<Check
size={18}
color="#FFFFFF"
strokeWidth={2.6}
opacity={isDirty ? 1 : 0.7}
/>
<Text className="text-base font-bold text-white ml-2 tracking-[0.2px]">
{t('profile.saveButton')}
</Text>
</View>
</Pressable>
</View>
</BottomSheetScrollView>
</BottomSheet>
);
}