feature & fix
deksripsi: feature: - Information Box - Create profile fix: component: Alet, Avatar, Select # No Issue
This commit is contained in:
@@ -110,7 +110,7 @@ const styles = StyleSheet.create({
|
||||
alertButton: {
|
||||
flex: 1,
|
||||
padding: 10,
|
||||
borderRadius: 5,
|
||||
borderRadius: 50,
|
||||
marginHorizontal: 5,
|
||||
alignItems: "center",
|
||||
},
|
||||
|
||||
33
components/Box/InformationBox.tsx
Normal file
33
components/Box/InformationBox.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import Grid from "../Grid/GridCustom";
|
||||
import TextCustom from "../Text/TextCustom";
|
||||
import BaseBox from "./BaseBox";
|
||||
|
||||
export default function InformationBox({ text }: { text: string }) {
|
||||
return (
|
||||
<>
|
||||
<BaseBox>
|
||||
<Grid>
|
||||
<Grid.Col
|
||||
span={2}
|
||||
style={{ alignItems: "center", justifyContent: "center" }}
|
||||
>
|
||||
<Ionicons
|
||||
name="information-circle-outline"
|
||||
size={24}
|
||||
color={MainColor.white}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={10} style={{ justifyContent: "center" }}>
|
||||
<TextCustom>
|
||||
{text
|
||||
? text
|
||||
: "Lorem ipsum dolor sit amet consectetur adipisicing elit."}
|
||||
</TextCustom>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</BaseBox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import DUMMY_IMAGE from "@/constants/dummy-image-value";
|
||||
import { Image, ImageSourcePropType, StyleSheet } from "react-native";
|
||||
|
||||
type Size = "base" | "sm" | "md" | "lg";
|
||||
type Size = "base" | "sm" | "md" | "lg" | "xl";
|
||||
|
||||
interface AvatarCustomProps {
|
||||
source?: ImageSourcePropType;
|
||||
@@ -13,10 +14,11 @@ const sizeMap = {
|
||||
sm: 60,
|
||||
md: 80,
|
||||
lg: 100,
|
||||
xl: 120,
|
||||
};
|
||||
|
||||
export default function AvatarCustom({
|
||||
source = require("@/assets/images/dummy/dummy-avatar.png"),
|
||||
source = DUMMY_IMAGE.avatar,
|
||||
size = "base",
|
||||
}: AvatarCustomProps) {
|
||||
const dimension = sizeMap[size];
|
||||
20
components/Image/LandscapeFrameUploaded.tsx
Normal file
20
components/Image/LandscapeFrameUploaded.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import DUMMY_IMAGE from "@/constants/dummy-image-value";
|
||||
import { Image } from "react-native";
|
||||
import BaseBox from "../Box/BaseBox";
|
||||
|
||||
export default function LandscapeFrameUploaded() {
|
||||
return (
|
||||
<BaseBox
|
||||
style={{
|
||||
height: 250,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={DUMMY_IMAGE.background}
|
||||
resizeMode="cover"
|
||||
style={{ width: "100%", height: "100%", borderRadius: 10 }}
|
||||
/>
|
||||
</BaseBox>
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,7 @@ type SelectProps = {
|
||||
placeholder?: string;
|
||||
data: SelectItem[];
|
||||
value?: string | number | null;
|
||||
required?: boolean; // <-- new prop
|
||||
onChange: (value: string | number) => void;
|
||||
};
|
||||
|
||||
@@ -30,16 +31,27 @@ const SelectCustom: React.FC<SelectProps> = ({
|
||||
placeholder = "Pilih opsi",
|
||||
data,
|
||||
value,
|
||||
required = false, // <-- default false
|
||||
onChange,
|
||||
}) => {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
|
||||
const selectedItem = data.find((item) => item.value === value);
|
||||
|
||||
const hasError = required && value === null; // <-- check if empty and required
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{label && <Text style={styles.label}>{label}</Text>}
|
||||
<Pressable style={styles.input} onPress={() => setModalVisible(true)}>
|
||||
{label && (
|
||||
<Text style={styles.label}>
|
||||
{label}
|
||||
{required && <Text style={styles.requiredIndicator}> *</Text>}
|
||||
</Text>
|
||||
)}
|
||||
<Pressable
|
||||
style={[styles.input, hasError ? styles.inputError : null]} // <-- add error style
|
||||
onPress={() => setModalVisible(true)}
|
||||
>
|
||||
<Text style={selectedItem ? styles.text : styles.placeholder}>
|
||||
{selectedItem?.label || placeholder}
|
||||
</Text>
|
||||
@@ -70,6 +82,11 @@ const SelectCustom: React.FC<SelectProps> = ({
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
|
||||
{/* Optional Error Message */}
|
||||
{hasError && (
|
||||
<Text style={styles.errorMessage}>Harap pilih salah satu</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -86,6 +103,10 @@ const styles = StyleSheet.create({
|
||||
color: MainColor.white,
|
||||
fontWeight: "500",
|
||||
},
|
||||
requiredIndicator: {
|
||||
color: "red",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: "#ccc",
|
||||
@@ -95,6 +116,9 @@ const styles = StyleSheet.create({
|
||||
justifyContent: "center",
|
||||
backgroundColor: MainColor.white,
|
||||
},
|
||||
inputError: {
|
||||
borderColor: "red",
|
||||
},
|
||||
text: {
|
||||
fontSize: TEXT_SIZE_MEDIUM,
|
||||
},
|
||||
@@ -120,4 +144,9 @@ const styles = StyleSheet.create({
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#eee",
|
||||
},
|
||||
errorMessage: {
|
||||
marginTop: 4,
|
||||
fontSize: 12,
|
||||
color: "red",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// components/TextInputCustom.tsx
|
||||
|
||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
@@ -32,15 +30,18 @@ export const TextInputCustom = ({
|
||||
iconRight,
|
||||
label,
|
||||
required = false,
|
||||
error = "",
|
||||
error: externalError = "",
|
||||
secureTextEntry = false,
|
||||
fontColor = "#000",
|
||||
disabled = false,
|
||||
borderRadius = 8,
|
||||
style,
|
||||
keyboardType,
|
||||
onChangeText,
|
||||
...rest
|
||||
}: Props) => {
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||
const [internalError, setInternalError] = useState("");
|
||||
|
||||
// Helper untuk render ikon
|
||||
const renderIcon = (icon: IconType) => {
|
||||
@@ -52,6 +53,23 @@ export const TextInputCustom = ({
|
||||
);
|
||||
};
|
||||
|
||||
// Validasi email jika keyboardType = email-address
|
||||
const handleTextChange = (text: string) => {
|
||||
if (keyboardType === "email-address") {
|
||||
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text);
|
||||
if (!isValid) {
|
||||
setInternalError("Masukkan email yang valid");
|
||||
} else {
|
||||
setInternalError("");
|
||||
}
|
||||
}
|
||||
|
||||
// Panggil onChangeText eksternal jika ada
|
||||
if (onChangeText) {
|
||||
onChangeText(text);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={textInputStyles.container}>
|
||||
{label && (
|
||||
@@ -65,7 +83,7 @@ export const TextInputCustom = ({
|
||||
textInputStyles.inputContainer,
|
||||
disabled && textInputStyles.disabled,
|
||||
{ borderRadius },
|
||||
error ? textInputStyles.errorBorder : null,
|
||||
externalError || internalError ? textInputStyles.errorBorder : null,
|
||||
style,
|
||||
]}
|
||||
>
|
||||
@@ -76,6 +94,8 @@ export const TextInputCustom = ({
|
||||
style={[textInputStyles.input, { color: fontColor }]}
|
||||
editable={!disabled}
|
||||
secureTextEntry={secureTextEntry && !isPasswordVisible}
|
||||
keyboardType={keyboardType}
|
||||
onChangeText={handleTextChange}
|
||||
{...rest}
|
||||
/>
|
||||
{secureTextEntry && (
|
||||
@@ -94,7 +114,12 @@ export const TextInputCustom = ({
|
||||
<View style={textInputStyles.icon}>{renderIcon(iconRight)}</View>
|
||||
)}
|
||||
</View>
|
||||
{error ? <Text style={textInputStyles.errorMessage}>{error}</Text> : null}
|
||||
{/* Prioritaskan error eksternal */}
|
||||
{externalError || internalError ? (
|
||||
<Text style={textInputStyles.errorMessage}>
|
||||
{externalError || internalError}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -47,42 +47,11 @@ const ViewWrapper = ({
|
||||
{tabBarComponent ? tabBarComponent : null}
|
||||
{bottomBarComponent ? (
|
||||
<View style={GStyles.bottomBar}>
|
||||
<View style={GStyles.bottomBarContainer}>
|
||||
{bottomBarComponent}
|
||||
</View>
|
||||
<View style={GStyles.bottomBarContainer}>{bottomBarComponent}</View>
|
||||
</View>
|
||||
) : null}
|
||||
</SafeAreaView>
|
||||
</>
|
||||
|
||||
// <SafeAreaProvider>
|
||||
// <SafeAreaView
|
||||
// edges={[
|
||||
// "bottom",
|
||||
// // "top",
|
||||
// ]}
|
||||
// style={{
|
||||
// flex: 1,
|
||||
// // paddingTop: StatusBar.currentHeight,
|
||||
// backgroundColor: MainColor.darkblue,
|
||||
// }}
|
||||
// >
|
||||
// <ScrollView contentContainerStyle={{ flexGrow: 1 }}>
|
||||
// {withBackground ? (
|
||||
// <ImageBackground
|
||||
// source={assetBackground}
|
||||
// resizeMode="cover"
|
||||
// style={Styles.imageBackground}
|
||||
// >
|
||||
// <View style={Styles.containerWithBackground}>{children}</View>
|
||||
// </ImageBackground>
|
||||
// ) : (
|
||||
// <View style={Styles.container}>{children}</View>
|
||||
// )}
|
||||
// </ScrollView>
|
||||
// {tabBarComponent}
|
||||
// </SafeAreaView>
|
||||
// </SafeAreaProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import Grid from "./Grid/GridCustom";
|
||||
// Box
|
||||
import BaseBox from "./Box/BaseBox";
|
||||
// Avatar
|
||||
import AvatarCustom from "./Avatar/AvatarCustom"
|
||||
import AvatarCustom from "./Image/AvatarCustom"
|
||||
// Stack
|
||||
import StackCustom from "./Stack/StackCustom";
|
||||
// Select
|
||||
|
||||
Reference in New Issue
Block a user