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:
parent
28d4fc176e
commit
07f25769e3
|
|
@ -6,12 +6,7 @@ import {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import { View, Pressable } from "react-native";
|
||||||
View,
|
|
||||||
Pressable,
|
|
||||||
StyleSheet,
|
|
||||||
Text as RNText,
|
|
||||||
} from "react-native";
|
|
||||||
import BottomSheet, {
|
import BottomSheet, {
|
||||||
BottomSheetScrollView,
|
BottomSheetScrollView,
|
||||||
BottomSheetTextInput,
|
BottomSheetTextInput,
|
||||||
|
|
@ -86,190 +81,226 @@ export const MapBottomSheet = forwardRef<BottomSheet, MapBottomSheetProps>(
|
||||||
}
|
}
|
||||||
}, [renamingScan, t]);
|
}, [renamingScan, t]);
|
||||||
|
|
||||||
|
const trimmedDraft = draftName.trim();
|
||||||
|
const initialDraft = renamingScan
|
||||||
|
? getScanDisplayName(renamingScan, t).trim()
|
||||||
|
: "";
|
||||||
|
const isDraftDirty =
|
||||||
|
trimmedDraft.length > 0 && trimmedDraft !== initialDraft;
|
||||||
|
|
||||||
function handleStartRename(scan: ScanRecord) {
|
function handleStartRename(scan: ScanRecord) {
|
||||||
setRenamingScan(scan);
|
setRenamingScan(scan);
|
||||||
// remonte à 85% pour bien voir l'input + boutons au-dessus du clavier
|
|
||||||
internalRef.current?.snapToIndex(2);
|
internalRef.current?.snapToIndex(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleConfirmRename() {
|
function handleConfirmRename() {
|
||||||
|
if (!isDraftDirty) return;
|
||||||
if (renamingScan) {
|
if (renamingScan) {
|
||||||
onRename?.(renamingScan.id, draftName);
|
onRename?.(renamingScan.id, trimmedDraft);
|
||||||
}
|
}
|
||||||
setRenamingScan(null);
|
setRenamingScan(null);
|
||||||
setDraftName("");
|
setDraftName("");
|
||||||
// redescend pour voir la map
|
|
||||||
internalRef.current?.snapToIndex(0);
|
internalRef.current?.snapToIndex(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCancelRename() {
|
function handleCancelRename() {
|
||||||
setRenamingScan(null);
|
setRenamingScan(null);
|
||||||
setDraftName("");
|
setDraftName("");
|
||||||
// redescend pour voir la map
|
|
||||||
internalRef.current?.snapToIndex(0);
|
internalRef.current?.snapToIndex(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<BottomSheet
|
||||||
<BottomSheet
|
ref={internalRef}
|
||||||
ref={internalRef}
|
index={defaultIndex}
|
||||||
index={defaultIndex}
|
snapPoints={snapPoints}
|
||||||
snapPoints={snapPoints}
|
handleIndicatorStyle={{
|
||||||
handleIndicatorStyle={styles.handleIndicator}
|
backgroundColor: colors.neutral[300],
|
||||||
backgroundStyle={styles.background}
|
width: 40,
|
||||||
containerStyle={styles.sheetContainer}
|
height: 4,
|
||||||
enableDynamicSizing={false}
|
}}
|
||||||
enablePanDownToClose={false}
|
backgroundStyle={{
|
||||||
keyboardBehavior="interactive"
|
backgroundColor: "#FFFFFF",
|
||||||
keyboardBlurBehavior="restore"
|
borderTopLeftRadius: 28,
|
||||||
android_keyboardInputMode="adjustResize"
|
borderTopRightRadius: 28,
|
||||||
>
|
}}
|
||||||
{renamingScan ? (
|
containerStyle={{ zIndex: 100, elevation: 100 }}
|
||||||
<BottomSheetScrollView
|
enableDynamicSizing={false}
|
||||||
contentContainerStyle={[
|
enablePanDownToClose={false}
|
||||||
styles.renameWrap,
|
keyboardBehavior="interactive"
|
||||||
{ paddingBottom: 24 },
|
keyboardBlurBehavior="restore"
|
||||||
]}
|
android_keyboardInputMode="adjustResize"
|
||||||
keyboardShouldPersistTaps="handled"
|
>
|
||||||
showsVerticalScrollIndicator={false}
|
{renamingScan ? (
|
||||||
>
|
<BottomSheetScrollView
|
||||||
<View style={styles.renameHeader}>
|
contentContainerStyle={{
|
||||||
<Pressable
|
paddingHorizontal: 20,
|
||||||
onPress={handleCancelRename}
|
paddingTop: 4,
|
||||||
hitSlop={10}
|
paddingBottom: 24,
|
||||||
style={styles.backBtn}
|
gap: 12,
|
||||||
>
|
}}
|
||||||
<ChevronLeft size={20} color={colors.neutral[700]} />
|
keyboardShouldPersistTaps="handled"
|
||||||
</Pressable>
|
showsVerticalScrollIndicator={false}
|
||||||
<Text style={styles.title}>{t("map.rename.title")}</Text>
|
>
|
||||||
<View style={styles.backBtnPlaceholder} />
|
<View className="flex-row items-center justify-between">
|
||||||
</View>
|
<Pressable
|
||||||
<Text style={styles.renameSubtitle}>
|
onPress={handleCancelRename}
|
||||||
{t("map.rename.subtitle")}
|
hitSlop={10}
|
||||||
|
className="w-9 h-9 rounded-full items-center justify-center bg-[#FAFAFA]"
|
||||||
|
>
|
||||||
|
<ChevronLeft size={20} color={colors.neutral[700]} />
|
||||||
|
</Pressable>
|
||||||
|
<Text className="text-lg font-bold text-[#1B1B1B]">
|
||||||
|
{t("map.rename.title")}
|
||||||
</Text>
|
</Text>
|
||||||
<BottomSheetTextInput
|
<View className="w-9 h-9" />
|
||||||
value={draftName}
|
</View>
|
||||||
onChangeText={setDraftName}
|
<Text className="text-[13px] leading-[18px] text-[#6B6B6B] px-1">
|
||||||
placeholder={t("map.rename.placeholder")}
|
{t("map.rename.subtitle")}
|
||||||
placeholderTextColor={colors.neutral[400]}
|
</Text>
|
||||||
autoFocus
|
<BottomSheetTextInput
|
||||||
maxLength={64}
|
value={draftName}
|
||||||
returnKeyType="done"
|
onChangeText={setDraftName}
|
||||||
onSubmitEditing={handleConfirmRename}
|
placeholder={t("map.rename.placeholder")}
|
||||||
style={styles.renameInput}
|
placeholderTextColor={colors.neutral[400]}
|
||||||
|
autoFocus
|
||||||
|
maxLength={64}
|
||||||
|
returnKeyType="done"
|
||||||
|
onSubmitEditing={handleConfirmRename}
|
||||||
|
style={{
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.neutral[300],
|
||||||
|
borderRadius: 14,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 14,
|
||||||
|
fontSize: 16,
|
||||||
|
color: colors.neutral[900],
|
||||||
|
backgroundColor: "#FAFAFA",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<View className="flex-row pt-4 gap-3">
|
||||||
|
<Pressable
|
||||||
|
onPress={handleCancelRename}
|
||||||
|
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={handleConfirmRename}
|
||||||
|
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 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")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</BottomSheetScrollView>
|
||||||
|
) : previewScan ? (
|
||||||
|
<View>
|
||||||
|
<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}
|
||||||
|
className="w-8 h-8 rounded-full items-center justify-center bg-[#FAFAFA]"
|
||||||
|
>
|
||||||
|
<X size={18} color={colors.neutral[600]} />
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
<View className="px-5 pb-5 gap-2">
|
||||||
|
<ScanRow
|
||||||
|
scan={previewScan}
|
||||||
|
isLast
|
||||||
|
onPress={() => onScanPress?.(previewScan)}
|
||||||
|
onEdit={() => handleStartRename(previewScan)}
|
||||||
/>
|
/>
|
||||||
<View style={styles.renameActions}>
|
<Text className="text-xs font-medium text-[#9E9E9E] text-center pt-1">
|
||||||
<Pressable
|
{t("map.preview.tapHint")}
|
||||||
onPress={handleCancelRename}
|
</Text>
|
||||||
style={({ pressed }) => [
|
</View>
|
||||||
styles.renameBtn,
|
</View>
|
||||||
styles.renameBtnGhost,
|
) : (
|
||||||
pressed && { opacity: 0.7 },
|
<>
|
||||||
]}
|
<View className="flex-row items-baseline justify-between px-5 pt-2 pb-3">
|
||||||
>
|
<Text className="text-lg font-bold text-[#1B1B1B]">
|
||||||
<View style={styles.renameBtnInner}>
|
{t("map.scannedPlants")}
|
||||||
<X
|
</Text>
|
||||||
size={18}
|
<Text className="text-[13px] font-medium text-[#9E9E9E]">
|
||||||
color={colors.neutral[800]}
|
{t("map.plantCount", { count: scans.length })}
|
||||||
strokeWidth={2.4}
|
</Text>
|
||||||
/>
|
|
||||||
<RNText style={styles.renameBtnGhostLabel}>
|
|
||||||
{t("common.cancel")}
|
|
||||||
</RNText>
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
<Pressable
|
|
||||||
onPress={handleConfirmRename}
|
|
||||||
style={({ pressed }) => [
|
|
||||||
styles.renameBtn,
|
|
||||||
styles.renameBtnPrimary,
|
|
||||||
pressed && { opacity: 0.85 },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View style={styles.renameBtnInner}>
|
|
||||||
<Check size={18} color="#FFFFFF" strokeWidth={2.6} />
|
|
||||||
<RNText style={styles.renameBtnPrimaryLabel}>
|
|
||||||
{t("map.rename.save")}
|
|
||||||
</RNText>
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
</BottomSheetScrollView>
|
|
||||||
) : previewScan ? (
|
|
||||||
<View>
|
|
||||||
<View style={styles.previewHeader}>
|
|
||||||
<Text style={styles.title}>{t("map.preview.title")}</Text>
|
|
||||||
<Pressable
|
|
||||||
onPress={onPreviewClose}
|
|
||||||
hitSlop={10}
|
|
||||||
style={styles.previewCloseBtn}
|
|
||||||
>
|
|
||||||
<X size={18} color={colors.neutral[600]} />
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
<View style={styles.previewBody}>
|
|
||||||
<ScanRow
|
|
||||||
scan={previewScan}
|
|
||||||
isLast
|
|
||||||
onPress={() => onScanPress?.(previewScan)}
|
|
||||||
onEdit={() => handleStartRename(previewScan)}
|
|
||||||
/>
|
|
||||||
<Text style={styles.previewHint}>
|
|
||||||
{t("map.preview.tapHint")}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<View style={styles.header}>
|
|
||||||
<Text style={styles.title}>{t("map.scannedPlants")}</Text>
|
|
||||||
<Text style={styles.count}>
|
|
||||||
{t("map.plantCount", { count: scans.length })}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{scans.length === 0 ? (
|
{scans.length === 0 ? (
|
||||||
<View style={styles.emptyState}>
|
<View className="items-center px-8 py-6 gap-2">
|
||||||
<View style={styles.emptyIconWrap}>
|
<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} />
|
<MapPin
|
||||||
|
size={32}
|
||||||
|
color={colors.primary[800]}
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<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}
|
||||||
|
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 className="text-white text-sm font-semibold">
|
||||||
|
{t("map.empty.cta")}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.emptyTitle}>{t("map.empty.title")}</Text>
|
) : (
|
||||||
<Text style={styles.emptySubtitle}>{t("map.empty.subtitle")}</Text>
|
<BottomSheetScrollView
|
||||||
{onScanCta && (
|
contentContainerStyle={{
|
||||||
<Pressable
|
paddingHorizontal: 20,
|
||||||
onPress={onScanCta}
|
paddingBottom: 24,
|
||||||
style={({ pressed }) => [
|
}}
|
||||||
styles.emptyCta,
|
showsVerticalScrollIndicator={false}
|
||||||
pressed && { opacity: 0.85 },
|
>
|
||||||
]}
|
{scans.map((scan, index) => (
|
||||||
>
|
<ScanRow
|
||||||
<ScanLine size={18} color="#FFFFFF" strokeWidth={2.2} />
|
key={scan.id}
|
||||||
<Text style={styles.emptyCtaLabel}>{t("map.empty.cta")}</Text>
|
scan={scan}
|
||||||
</Pressable>
|
isLast={index === scans.length - 1}
|
||||||
)}
|
onPress={() => onScanPress?.(scan)}
|
||||||
</View>
|
onEdit={() => handleStartRename(scan)}
|
||||||
) : (
|
/>
|
||||||
<BottomSheetScrollView
|
))}
|
||||||
contentContainerStyle={styles.listContent}
|
</BottomSheetScrollView>
|
||||||
showsVerticalScrollIndicator={false}
|
)}
|
||||||
>
|
</>
|
||||||
{scans.map((scan, index) => (
|
)}
|
||||||
<ScanRow
|
</BottomSheet>
|
||||||
key={scan.id}
|
|
||||||
scan={scan}
|
|
||||||
isLast={index === scans.length - 1}
|
|
||||||
onPress={() => onScanPress?.(scan)}
|
|
||||||
onEdit={() => handleStartRename(scan)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</BottomSheetScrollView>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</BottomSheet>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const STATUS_TINT: Record<ScanStatus, { bg: string; fg: string }> = {
|
const STATUS_TINT: Record<ScanStatus, { bg: string; fg: string }> = {
|
||||||
|
|
@ -301,23 +332,40 @@ function ScanRow({ scan, isLast, onPress, onEdit }: ScanRowProps) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.row, !isLast && styles.rowBorder]}>
|
<View
|
||||||
<Pressable onPress={onPress} style={styles.rowMain}>
|
className={`flex-row items-center gap-2 py-3.5 ${
|
||||||
<View style={[styles.iconBadge, { backgroundColor: tint.bg }]}>
|
!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} />
|
<Icon size={22} color={tint.fg} strokeWidth={2.2} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.rowText}>
|
<View className="flex-1 gap-0.5">
|
||||||
<Text style={styles.location} numberOfLines={1}>
|
<Text
|
||||||
|
className="text-[15px] font-bold text-[#1B1B1B]"
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
{displayName}
|
{displayName}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.region} numberOfLines={1}>
|
<Text className="text-[13px] text-[#6B6B6B]" numberOfLines={1}>
|
||||||
{formattedDate}
|
{formattedDate}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</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} />
|
<Pencil size={16} color={colors.neutral[600]} strokeWidth={2} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable onPress={onPress} hitSlop={8}>
|
<Pressable onPress={onPress} hitSlop={8}>
|
||||||
|
|
@ -326,249 +374,3 @@ function ScanRow({ scan, isLast, onPress, onEdit }: ScanRowProps) {
|
||||||
</View>
|
</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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,10 @@ import {
|
||||||
View,
|
View,
|
||||||
Pressable,
|
Pressable,
|
||||||
Modal,
|
Modal,
|
||||||
KeyboardAvoidingView,
|
|
||||||
Platform,
|
|
||||||
TextInput,
|
TextInput,
|
||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
Keyboard,
|
Keyboard,
|
||||||
|
Animated,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
@ -30,11 +29,25 @@ export function EditNameBottomSheet({
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const [name, setName] = useState(initialName);
|
const [name, setName] = useState(initialName);
|
||||||
|
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setName(initialName);
|
setName(initialName);
|
||||||
}, [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 trimmedName = name.trim();
|
||||||
const isDirty =
|
const isDirty =
|
||||||
trimmedName.length > 0 && trimmedName !== initialName.trim();
|
trimmedName.length > 0 && trimmedName !== initialName.trim();
|
||||||
|
|
@ -44,97 +57,97 @@ export function EditNameBottomSheet({
|
||||||
onSave(trimmedName);
|
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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible
|
visible
|
||||||
transparent
|
transparent
|
||||||
animationType="slide"
|
animationType="fade"
|
||||||
onRequestClose={onClose}
|
onRequestClose={onClose}
|
||||||
statusBarTranslucent
|
statusBarTranslucent
|
||||||
>
|
>
|
||||||
<KeyboardAvoidingView
|
<View className="flex-1 justify-end">
|
||||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
||||||
className="flex-1 justify-end"
|
|
||||||
>
|
|
||||||
<TouchableWithoutFeedback onPress={onClose}>
|
<TouchableWithoutFeedback onPress={onClose}>
|
||||||
<View className="absolute inset-0 bg-black/40" />
|
<View className="absolute inset-0 bg-black/40" />
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
|
|
||||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
<Animated.View
|
||||||
<View
|
className="bg-white rounded-t-[28px] px-5 pt-3"
|
||||||
className="bg-white rounded-t-[28px] px-5 pt-3"
|
style={{ paddingBottom: bottomPadding }}
|
||||||
style={{ paddingBottom: insets.bottom + 24 }}
|
>
|
||||||
>
|
<View className="items-center pb-2">
|
||||||
<View className="items-center pb-2">
|
<View className="w-10 h-1 rounded-full bg-[#E0E0E0]" />
|
||||||
<View className="w-10 h-1 rounded-full bg-[#E0E0E0]" />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="flex-row items-center justify-between mb-3">
|
|
||||||
<Text className="text-lg font-bold text-[#1B1B1B]">
|
|
||||||
{t("myPlants.detail.renameTitle")}
|
|
||||||
</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 className="text-[13px] leading-[18px] text-[#6B6B6B] mb-3">
|
|
||||||
{t("myPlants.detail.renameSubtitle")}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
value={name}
|
|
||||||
onChangeText={setName}
|
|
||||||
placeholder={t("myPlants.detail.renamePlaceholder")}
|
|
||||||
placeholderTextColor={colors.neutral[400]}
|
|
||||||
autoFocus
|
|
||||||
maxLength={64}
|
|
||||||
returnKeyType="done"
|
|
||||||
onSubmitEditing={handleSave}
|
|
||||||
className="border border-[#E0E0E0] rounded-[14px] px-3.5 py-3.5 text-base text-[#1B1B1B] bg-[#FAFAFA]"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<View className="flex-row pt-4 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("myPlants.actions.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("myPlants.detail.renameSave")}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</TouchableWithoutFeedback>
|
|
||||||
</KeyboardAvoidingView>
|
<View className="flex-row items-center justify-between mb-3">
|
||||||
|
<Text className="text-lg font-bold text-[#1B1B1B]">
|
||||||
|
{t("myPlants.detail.renameTitle")}
|
||||||
|
</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 className="text-[13px] leading-[18px] text-[#6B6B6B] mb-3">
|
||||||
|
{t("myPlants.detail.renameSubtitle")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
value={name}
|
||||||
|
onChangeText={setName}
|
||||||
|
placeholder={t("myPlants.detail.renamePlaceholder")}
|
||||||
|
placeholderTextColor={colors.neutral[400]}
|
||||||
|
autoFocus
|
||||||
|
maxLength={64}
|
||||||
|
returnKeyType="done"
|
||||||
|
onSubmitEditing={handleSave}
|
||||||
|
className="border border-[#E0E0E0] rounded-[14px] px-3.5 py-3.5 text-base text-[#1B1B1B] bg-[#FAFAFA]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View className="flex-row pt-4 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("myPlants.actions.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("myPlants.detail.renameSave")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import { View, Pressable } from 'react-native';
|
||||||
View,
|
|
||||||
Pressable,
|
|
||||||
StyleSheet,
|
|
||||||
Text as RNText,
|
|
||||||
} from 'react-native';
|
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import BottomSheet, {
|
import BottomSheet, {
|
||||||
BottomSheetScrollView,
|
BottomSheetScrollView,
|
||||||
|
|
@ -56,9 +51,15 @@ export function EditProfileModal({
|
||||||
}
|
}
|
||||||
}, [visible, initialProfile]);
|
}, [visible, initialProfile]);
|
||||||
|
|
||||||
|
const trimmedName = displayName.trim();
|
||||||
|
const trimmedEmail = email.trim();
|
||||||
|
const isDirty =
|
||||||
|
trimmedName !== initialProfile.displayName.trim() ||
|
||||||
|
trimmedEmail !== initialProfile.email.trim() ||
|
||||||
|
avatar !== initialProfile.avatar;
|
||||||
|
|
||||||
function handleSave() {
|
function handleSave() {
|
||||||
const trimmedName = displayName.trim();
|
if (!isDirty) return;
|
||||||
const trimmedEmail = email.trim();
|
|
||||||
|
|
||||||
if (trimmedEmail.length > 0 && !isValidEmail(trimmedEmail)) {
|
if (trimmedEmail.length > 0 && !isValidEmail(trimmedEmail)) {
|
||||||
toast.error(t('profile.invalidEmail'));
|
toast.error(t('profile.invalidEmail'));
|
||||||
|
|
@ -86,237 +87,151 @@ export function EditProfileModal({
|
||||||
keyboardBlurBehavior="restore"
|
keyboardBlurBehavior="restore"
|
||||||
android_keyboardInputMode="adjustResize"
|
android_keyboardInputMode="adjustResize"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
backgroundStyle={styles.background}
|
backgroundStyle={{
|
||||||
handleIndicatorStyle={styles.handle}
|
backgroundColor: '#FFFFFF',
|
||||||
containerStyle={styles.container}
|
borderTopLeftRadius: 28,
|
||||||
|
borderTopRightRadius: 28,
|
||||||
|
}}
|
||||||
|
handleIndicatorStyle={{
|
||||||
|
backgroundColor: colors.neutral[300],
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
}}
|
||||||
|
containerStyle={{ zIndex: 100, elevation: 100 }}
|
||||||
>
|
>
|
||||||
<BottomSheetScrollView
|
<BottomSheetScrollView
|
||||||
contentContainerStyle={[
|
contentContainerStyle={{
|
||||||
styles.content,
|
paddingHorizontal: 20,
|
||||||
{ paddingBottom: insets.bottom + 24 },
|
paddingTop: 4,
|
||||||
]}
|
paddingBottom: insets.bottom + 24,
|
||||||
|
gap: 12,
|
||||||
|
}}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
<View style={styles.header}>
|
<View className="flex-row items-center justify-between">
|
||||||
<Text style={styles.title}>{t('profile.editTitle')}</Text>
|
<Text className="text-lg font-bold text-[#1B1B1B]">
|
||||||
<Pressable onPress={onClose} hitSlop={10} style={styles.closeBtn}>
|
{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]} />
|
<X size={18} color={colors.neutral[600]} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={styles.fieldLabel}>{t('profile.avatarLabel')}</Text>
|
<Text className="text-xs font-semibold text-[#9E9E9E] uppercase tracking-[1px] mt-3 mb-2">
|
||||||
<View style={styles.avatarRow}>
|
{t('profile.avatarLabel')}
|
||||||
{AVATAR_OPTIONS.map((option) => {
|
</Text>
|
||||||
const selected = option === avatar;
|
<View className="flex-row flex-wrap gap-2.5">
|
||||||
return (
|
{AVATAR_OPTIONS.map((option) => {
|
||||||
<Pressable
|
const selected = option === avatar;
|
||||||
key={option}
|
return (
|
||||||
onPress={() => setAvatar(option)}
|
<Pressable
|
||||||
style={[
|
key={option}
|
||||||
styles.avatarOption,
|
onPress={() => setAvatar(option)}
|
||||||
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
|
||||||
|
className="text-[28px] text-center"
|
||||||
|
style={{ lineHeight: 36, includeFontPadding: false }}
|
||||||
>
|
>
|
||||||
<Text style={styles.avatarEmoji}>{option}</Text>
|
{option}
|
||||||
</Pressable>
|
</Text>
|
||||||
);
|
</Pressable>
|
||||||
})}
|
);
|
||||||
</View>
|
})}
|
||||||
|
</View>
|
||||||
|
|
||||||
<Text style={styles.fieldLabel}>{t('profile.nameField')}</Text>
|
<Text className="text-xs font-semibold text-[#9E9E9E] uppercase tracking-[1px] mt-3 mb-2">
|
||||||
<BottomSheetTextInput
|
{t('profile.nameField')}
|
||||||
style={styles.input}
|
</Text>
|
||||||
value={displayName}
|
<BottomSheetTextInput
|
||||||
onChangeText={setDisplayName}
|
value={displayName}
|
||||||
placeholder={t('profile.namePlaceholder')}
|
onChangeText={setDisplayName}
|
||||||
placeholderTextColor={colors.neutral[400]}
|
placeholder={t('profile.namePlaceholder')}
|
||||||
maxLength={64}
|
placeholderTextColor={colors.neutral[400]}
|
||||||
returnKeyType="next"
|
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">
|
||||||
<BottomSheetTextInput
|
{t('profile.emailField')}
|
||||||
style={styles.input}
|
</Text>
|
||||||
value={email}
|
<BottomSheetTextInput
|
||||||
onChangeText={setEmail}
|
value={email}
|
||||||
placeholder={t('profile.emailPlaceholder')}
|
onChangeText={setEmail}
|
||||||
placeholderTextColor={colors.neutral[400]}
|
placeholder={t('profile.emailPlaceholder')}
|
||||||
keyboardType="email-address"
|
placeholderTextColor={colors.neutral[400]}
|
||||||
autoCapitalize="none"
|
keyboardType="email-address"
|
||||||
autoCorrect={false}
|
autoCapitalize="none"
|
||||||
maxLength={128}
|
autoCorrect={false}
|
||||||
returnKeyType="done"
|
maxLength={128}
|
||||||
onSubmitEditing={handleSave}
|
returnKeyType="done"
|
||||||
/>
|
onSubmitEditing={handleSave}
|
||||||
|
style={{
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.neutral[300],
|
||||||
|
borderRadius: 14,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 14,
|
||||||
|
fontSize: 16,
|
||||||
|
color: colors.neutral[900],
|
||||||
|
backgroundColor: '#FAFAFA',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<View style={styles.actions}>
|
<View className="flex-row pt-5 gap-3">
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onClose}
|
onPress={onClose}
|
||||||
style={({ pressed }) => [
|
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"
|
||||||
styles.button,
|
>
|
||||||
styles.buttonGhost,
|
<View className="flex-row items-center">
|
||||||
pressed && { opacity: 0.7 },
|
<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')}
|
||||||
<View style={styles.buttonInner}>
|
</Text>
|
||||||
<X size={18} color={colors.neutral[800]} strokeWidth={2.4} />
|
</View>
|
||||||
<RNText style={styles.buttonGhostLabel}>
|
</Pressable>
|
||||||
{t('common.cancel')}
|
<Pressable
|
||||||
</RNText>
|
onPress={handleSave}
|
||||||
</View>
|
disabled={!isDirty}
|
||||||
</Pressable>
|
className={
|
||||||
<Pressable
|
isDirty
|
||||||
onPress={handleSave}
|
? 'flex-1 min-h-[56px] rounded-[14px] py-4 px-3 items-center justify-center bg-primary active:opacity-85'
|
||||||
style={({ pressed }) => [
|
: 'flex-1 min-h-[56px] rounded-[14px] py-4 px-3 items-center justify-center bg-primary/40'
|
||||||
styles.button,
|
}
|
||||||
styles.buttonPrimary,
|
>
|
||||||
pressed && { opacity: 0.85 },
|
<View className="flex-row items-center">
|
||||||
]}
|
<Check
|
||||||
>
|
size={18}
|
||||||
<View style={styles.buttonInner}>
|
color="#FFFFFF"
|
||||||
<Check size={18} color="#FFFFFF" strokeWidth={2.6} />
|
strokeWidth={2.6}
|
||||||
<RNText style={styles.buttonPrimaryLabel}>
|
opacity={isDirty ? 1 : 0.7}
|
||||||
{t('profile.saveButton')}
|
/>
|
||||||
</RNText>
|
<Text className="text-base font-bold text-white ml-2 tracking-[0.2px]">
|
||||||
</View>
|
{t('profile.saveButton')}
|
||||||
</Pressable>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
</BottomSheetScrollView>
|
</BottomSheetScrollView>
|
||||||
</BottomSheet>
|
</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: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.neutral[300],
|
|
||||||
borderRadius: 14,
|
|
||||||
paddingHorizontal: 14,
|
|
||||||
paddingVertical: 14,
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue