feature & fix

deksripsi:
feature:
- Information Box
- Create profile
fix:
component: Alet, Avatar, Select
# No Issue
This commit is contained in:
2025-07-04 17:42:22 +08:00
parent 0b1fd05eec
commit 1a16b16f47
13 changed files with 233 additions and 48 deletions

View File

@@ -0,0 +1,97 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
AvatarCustom,
ButtonCustom,
Spacing,
StackCustom,
SelectCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import InformationBox from "@/components/Box/InformationBox";
import ButtonUpload from "@/components/Button/ButtonUpload";
import LandscapeFrameUploaded from "@/components/Image/LandscapeFrameUploaded";
import { View } from "react-native";
import { useState } from "react";
import { router } from "expo-router";
export default function CreateProfile() {
const [data, setData] = useState({
name: "",
email: "",
address: "",
gender: "",
});
const handlerSave = () => {
console.log("data create profile >>", data);
// router.back();
};
return (
<ViewWrapper
bottomBarComponent={
<ButtonCustom
onPress={handlerSave}
disabled={!data.name || !data.email || !data.address || !data.gender}
>
Simpan
</ButtonCustom>
}
>
<StackCustom>
<InformationBox text="Upload foto profile anda." />
<View style={{ alignItems: "center" }}>
<AvatarCustom size="xl" />
<Spacing />
<ButtonUpload onPress={() => console.log("pressed")} />
</View>
<Spacing />
<View>
<InformationBox text="Upload foto latar belakang anda." />
<LandscapeFrameUploaded />
<Spacing />
<ButtonUpload onPress={() => console.log("pressed")} />
</View>
<Spacing />
<TextInputCustom
required
label="Nama"
placeholder="Masukkan nama"
value={data.name}
onChangeText={(text) => setData({ ...data, name: text })}
/>
<TextInputCustom
keyboardType="email-address"
required
label="Email"
placeholder="Masukkan email"
value={data.email}
onChangeText={(text) => setData({ ...data, email: text })}
/>
<TextInputCustom
required
label="Alamat"
placeholder="Masukkan alamat"
value={data.address}
onChangeText={(text) => setData({ ...data, address: text })}
/>
<SelectCustom
label="Jenis Kelamin"
placeholder="Pilih jenis kelamin"
data={[
{ label: "Laki-laki", value: "laki-laki" },
{ label: "Perempuan", value: "perempuan" },
]}
value={data.gender}
required
onChange={(value) => setData({ ...(data as any), gender: value })}
/>
<Spacing height={100} />
</StackCustom>
</ViewWrapper>
);
}

View File

@@ -45,6 +45,11 @@ export default function Profile() {
// path: `/(application)/profile/dashboard-admin`, // path: `/(application)/profile/dashboard-admin`,
// }, // },
{ icon: "log-out", label: "Keluar", color: "red", path: "" }, { icon: "log-out", label: "Keluar", color: "red", path: "" },
{
icon: "create-outline",
label: "Create profile",
path: `/(application)/profile/${id}/create`,
},
]; ];
// Animasi menggunakan translateY (lebih kompatibel) // Animasi menggunakan translateY (lebih kompatibel)

View File

@@ -24,6 +24,10 @@ export default function ProfileLayout() {
name="[id]/update-background" name="[id]/update-background"
options={{ title: "Update Latar Belakang" }} options={{ title: "Update Latar Belakang" }}
/> />
<Stack.Screen
name="[id]/create"
options={{ title: "Buat Profile" }}
/>
</Stack> </Stack>
</> </>
); );

View File

@@ -110,7 +110,7 @@ const styles = StyleSheet.create({
alertButton: { alertButton: {
flex: 1, flex: 1,
padding: 10, padding: 10,
borderRadius: 5, borderRadius: 50,
marginHorizontal: 5, marginHorizontal: 5,
alignItems: "center", alignItems: "center",
}, },

View 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>
</>
);
}

View File

@@ -1,7 +1,8 @@
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { Image, ImageSourcePropType, StyleSheet } from "react-native"; import { Image, ImageSourcePropType, StyleSheet } from "react-native";
type Size = "base" | "sm" | "md" | "lg"; type Size = "base" | "sm" | "md" | "lg" | "xl";
interface AvatarCustomProps { interface AvatarCustomProps {
source?: ImageSourcePropType; source?: ImageSourcePropType;
@@ -13,10 +14,11 @@ const sizeMap = {
sm: 60, sm: 60,
md: 80, md: 80,
lg: 100, lg: 100,
xl: 120,
}; };
export default function AvatarCustom({ export default function AvatarCustom({
source = require("@/assets/images/dummy/dummy-avatar.png"), source = DUMMY_IMAGE.avatar,
size = "base", size = "base",
}: AvatarCustomProps) { }: AvatarCustomProps) {
const dimension = sizeMap[size]; const dimension = sizeMap[size];

View 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>
);
}

View File

@@ -22,6 +22,7 @@ type SelectProps = {
placeholder?: string; placeholder?: string;
data: SelectItem[]; data: SelectItem[];
value?: string | number | null; value?: string | number | null;
required?: boolean; // <-- new prop
onChange: (value: string | number) => void; onChange: (value: string | number) => void;
}; };
@@ -30,16 +31,27 @@ const SelectCustom: React.FC<SelectProps> = ({
placeholder = "Pilih opsi", placeholder = "Pilih opsi",
data, data,
value, value,
required = false, // <-- default false
onChange, onChange,
}) => { }) => {
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
const selectedItem = data.find((item) => item.value === value); const selectedItem = data.find((item) => item.value === value);
const hasError = required && value === null; // <-- check if empty and required
return ( return (
<View style={styles.container}> <View style={styles.container}>
{label && <Text style={styles.label}>{label}</Text>} {label && (
<Pressable style={styles.input} onPress={() => setModalVisible(true)}> <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}> <Text style={selectedItem ? styles.text : styles.placeholder}>
{selectedItem?.label || placeholder} {selectedItem?.label || placeholder}
</Text> </Text>
@@ -70,6 +82,11 @@ const SelectCustom: React.FC<SelectProps> = ({
</View> </View>
</TouchableOpacity> </TouchableOpacity>
</Modal> </Modal>
{/* Optional Error Message */}
{hasError && (
<Text style={styles.errorMessage}>Harap pilih salah satu</Text>
)}
</View> </View>
); );
}; };
@@ -86,6 +103,10 @@ const styles = StyleSheet.create({
color: MainColor.white, color: MainColor.white,
fontWeight: "500", fontWeight: "500",
}, },
requiredIndicator: {
color: "red",
fontWeight: "bold",
},
input: { input: {
borderWidth: 1, borderWidth: 1,
borderColor: "#ccc", borderColor: "#ccc",
@@ -95,6 +116,9 @@ const styles = StyleSheet.create({
justifyContent: "center", justifyContent: "center",
backgroundColor: MainColor.white, backgroundColor: MainColor.white,
}, },
inputError: {
borderColor: "red",
},
text: { text: {
fontSize: TEXT_SIZE_MEDIUM, fontSize: TEXT_SIZE_MEDIUM,
}, },
@@ -120,4 +144,9 @@ const styles = StyleSheet.create({
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: "#eee", borderBottomColor: "#eee",
}, },
errorMessage: {
marginTop: 4,
fontSize: 12,
color: "red",
},
}); });

View File

@@ -1,5 +1,3 @@
// components/TextInputCustom.tsx
import Ionicons from "@expo/vector-icons/Ionicons"; import Ionicons from "@expo/vector-icons/Ionicons";
import React, { useState } from "react"; import React, { useState } from "react";
import { import {
@@ -32,15 +30,18 @@ export const TextInputCustom = ({
iconRight, iconRight,
label, label,
required = false, required = false,
error = "", error: externalError = "",
secureTextEntry = false, secureTextEntry = false,
fontColor = "#000", fontColor = "#000",
disabled = false, disabled = false,
borderRadius = 8, borderRadius = 8,
style, style,
keyboardType,
onChangeText,
...rest ...rest
}: Props) => { }: Props) => {
const [isPasswordVisible, setIsPasswordVisible] = useState(false); const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const [internalError, setInternalError] = useState("");
// Helper untuk render ikon // Helper untuk render ikon
const renderIcon = (icon: IconType) => { 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 ( return (
<View style={textInputStyles.container}> <View style={textInputStyles.container}>
{label && ( {label && (
@@ -65,7 +83,7 @@ export const TextInputCustom = ({
textInputStyles.inputContainer, textInputStyles.inputContainer,
disabled && textInputStyles.disabled, disabled && textInputStyles.disabled,
{ borderRadius }, { borderRadius },
error ? textInputStyles.errorBorder : null, externalError || internalError ? textInputStyles.errorBorder : null,
style, style,
]} ]}
> >
@@ -76,6 +94,8 @@ export const TextInputCustom = ({
style={[textInputStyles.input, { color: fontColor }]} style={[textInputStyles.input, { color: fontColor }]}
editable={!disabled} editable={!disabled}
secureTextEntry={secureTextEntry && !isPasswordVisible} secureTextEntry={secureTextEntry && !isPasswordVisible}
keyboardType={keyboardType}
onChangeText={handleTextChange}
{...rest} {...rest}
/> />
{secureTextEntry && ( {secureTextEntry && (
@@ -94,7 +114,12 @@ export const TextInputCustom = ({
<View style={textInputStyles.icon}>{renderIcon(iconRight)}</View> <View style={textInputStyles.icon}>{renderIcon(iconRight)}</View>
)} )}
</View> </View>
{error ? <Text style={textInputStyles.errorMessage}>{error}</Text> : null} {/* Prioritaskan error eksternal */}
{externalError || internalError ? (
<Text style={textInputStyles.errorMessage}>
{externalError || internalError}
</Text>
) : null}
</View> </View>
); );
}; };

View File

@@ -47,42 +47,11 @@ const ViewWrapper = ({
{tabBarComponent ? tabBarComponent : null} {tabBarComponent ? tabBarComponent : null}
{bottomBarComponent ? ( {bottomBarComponent ? (
<View style={GStyles.bottomBar}> <View style={GStyles.bottomBar}>
<View style={GStyles.bottomBarContainer}> <View style={GStyles.bottomBarContainer}>{bottomBarComponent}</View>
{bottomBarComponent}
</View>
</View> </View>
) : null} ) : null}
</SafeAreaView> </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>
); );
}; };

View File

@@ -18,7 +18,7 @@ import Grid from "./Grid/GridCustom";
// Box // Box
import BaseBox from "./Box/BaseBox"; import BaseBox from "./Box/BaseBox";
// Avatar // Avatar
import AvatarCustom from "./Avatar/AvatarCustom" import AvatarCustom from "./Image/AvatarCustom"
// Stack // Stack
import StackCustom from "./Stack/StackCustom"; import StackCustom from "./Stack/StackCustom";
// Select // Select

View File

@@ -28,10 +28,10 @@ export default function LoginView() {
const randomAlfabet = Math.random().toString(36).substring(2, 8); const randomAlfabet = Math.random().toString(36).substring(2, 8);
const randomNumber = Math.floor(Math.random() * 1000000); const randomNumber = Math.floor(Math.random() * 1000000);
const id = randomAlfabet + randomNumber + fixNumber; const id = randomAlfabet + randomNumber + fixNumber;
console.log("user id :", id); console.log("login user id :", id);
// router.navigate("/verification"); router.navigate("/verification");
router.navigate(`/(application)/profile/${id}`); // router.navigate(`/(application)/profile/${id}`);
// router.navigate("/(application)/home"); // router.navigate("/(application)/home");
// router.navigate(`/(application)/profile/${id}/edit`); // router.navigate(`/(application)/profile/${id}/edit`);

View File

@@ -1,12 +1,13 @@
import { AvatarCustom } from "@/components"; import { AvatarCustom } from "@/components";
import { AccentColor } from "@/constants/color-palet"; import { AccentColor } from "@/constants/color-palet";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { View, ImageBackground, StyleSheet } from "react-native"; import { View, ImageBackground, StyleSheet } from "react-native";
const AvatarAndBackground = () => { const AvatarAndBackground = () => {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<ImageBackground <ImageBackground
source={require("@/assets/images/logo-hipmi.png")} source={DUMMY_IMAGE.background}
style={styles.backgroundImage} style={styles.backgroundImage}
resizeMode="contain" resizeMode="contain"
/> />
@@ -15,7 +16,7 @@ const AvatarAndBackground = () => {
{/* Avatar yang sedikit keluar */} {/* Avatar yang sedikit keluar */}
<View style={styles.avatarOverlap}> <View style={styles.avatarOverlap}>
<AvatarCustom <AvatarCustom
source={require("@/assets/images/react-logo.png")} source={DUMMY_IMAGE.avatar}
size="lg" size="lg"
/> />
</View> </View>