From 258e20751e0ed136ad65bb64f04e45e73aa3e85d Mon Sep 17 00:00:00 2001 From: Bagasbanuna02 Date: Tue, 1 Jul 2025 17:47:51 +0800 Subject: [PATCH] feature profile deskripsi: - drawer & alert - screen baru: edit profile, update photo, update background, create portofolio --- app/(application)/portofolio/[id].tsx | 9 + app/(application)/portofolio/create/[id].tsx | 11 + app/(application)/profile/[id].tsx | 306 ++++-------------- app/(application)/profile/edit.tsx | 9 + .../profile/update-background/[id].tsx | 11 + .../profile/update-photo/[id].tsx | 11 + components/Alert/AlertCustom.tsx | 127 ++++++++ components/Drawer/DrawerCustom.tsx | 150 +++++++++ components/Drawer/MenuDrawerDynamicGird.tsx | 57 ++++ components/_Interface/types.ts | 9 +- constants/constans-value.ts | 15 + eas.build.android | 1 + eas.json | 7 +- screens/Profile/menuDrawerSection.tsx | 31 ++ 14 files changed, 510 insertions(+), 244 deletions(-) create mode 100644 app/(application)/portofolio/[id].tsx create mode 100644 app/(application)/portofolio/create/[id].tsx create mode 100644 app/(application)/profile/edit.tsx create mode 100644 app/(application)/profile/update-background/[id].tsx create mode 100644 app/(application)/profile/update-photo/[id].tsx create mode 100644 components/Alert/AlertCustom.tsx create mode 100644 components/Drawer/DrawerCustom.tsx create mode 100644 components/Drawer/MenuDrawerDynamicGird.tsx create mode 100644 constants/constans-value.ts create mode 100644 eas.build.android create mode 100644 screens/Profile/menuDrawerSection.tsx diff --git a/app/(application)/portofolio/[id].tsx b/app/(application)/portofolio/[id].tsx new file mode 100644 index 0000000..9b2bf63 --- /dev/null +++ b/app/(application)/portofolio/[id].tsx @@ -0,0 +1,9 @@ +import { Text, View } from "react-native"; + +export default function Portofolio() { + return ( + + Portofolio + + ); +} \ No newline at end of file diff --git a/app/(application)/portofolio/create/[id].tsx b/app/(application)/portofolio/create/[id].tsx new file mode 100644 index 0000000..2b08612 --- /dev/null +++ b/app/(application)/portofolio/create/[id].tsx @@ -0,0 +1,11 @@ +import { Text, View } from "react-native"; +import { useLocalSearchParams } from "expo-router"; + +export default function PortofolioCreate() { + const { id } = useLocalSearchParams(); + return ( + + Portofolio Create {id} + + ); +} \ No newline at end of file diff --git a/app/(application)/profile/[id].tsx b/app/(application)/profile/[id].tsx index 0bc1a54..d7f5885 100644 --- a/app/(application)/profile/[id].tsx +++ b/app/(application)/profile/[id].tsx @@ -1,25 +1,51 @@ + +import { IMenuDrawerItem } from "@/components/_Interface/types"; import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; -import { AccentColor, MainColor } from "@/constants/color-palet"; +import AlertCustom from "@/components/Alert/AlertCustom"; +import DrawerCustom from "@/components/Drawer/DrawerCustom"; +import { MainColor } from "@/constants/color-palet"; +import { DRAWER_HEIGHT } from "@/constants/constans-value"; +import Profile_MenuDrawerSection from "@/screens/Profile/menuDrawerSection"; import { Styles } from "@/styles/global-styles"; import { Ionicons } from "@expo/vector-icons"; import { router, Stack, useLocalSearchParams } from "expo-router"; import React, { useRef, useState } from "react"; -import { - Alert, - Animated, - PanResponder, - StyleSheet, - Text, - TouchableOpacity, - View, -} from "react-native"; +import { Animated, Text, TouchableOpacity } from "react-native"; -const DRAWER_HEIGHT = 300; // tinggi drawer export default function Profile() { const { id } = useLocalSearchParams(); const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [showLogoutAlert, setShowLogoutAlert] = useState(false); + const drawerItems: IMenuDrawerItem[] = [ + { + icon: "create", + label: "Edit profile", + path: "/(application)/profile/edit", + }, + { + icon: "camera", + label: "Ubah foto profile", + path: `/(application)/profile/update-photo/${id}`, + }, + { + icon: "image", + label: "Ubah latar belakang", + path: `/(application)/profile/update-background/${id}`, + }, + { + icon: "add-circle", + label: "Tambah portofolio", + path: `/(application)/portofolio/create/${id}`, + }, + // { + // icon: "settings", + // label: "Dashboard Admin", + // path: `/(application)/profile/dashboard-admin`, + // }, + { icon: "log-out", label: "Keluar", color: "red", path: "" }, + ]; + // Animasi menggunakan translateY (lebih kompatibel) const drawerAnim = useRef(new Animated.Value(DRAWER_HEIGHT)).current; // mulai di luar bawah layar @@ -42,35 +68,16 @@ export default function Profile() { }); }; - const panResponder = useRef( - PanResponder.create({ - onMoveShouldSetPanResponder: (_, gestureState) => { - return gestureState.dy > 10; // gesek ke bawah - }, - onPanResponderMove: (_, gestureState) => { - const offset = gestureState.dy; - if (offset >= 0 && offset <= DRAWER_HEIGHT) { - drawerAnim.setValue(offset); // batasi hingga max 500 - } - }, - onPanResponderRelease: (_, gestureState) => { - if (gestureState.dy > 200) { - // Tutup drawer sepenuhnya jika gesek lebih dari 200 - closeDrawer(); - } else { - // Reset ke posisi awal jika gesek kurang - Animated.spring(drawerAnim, { - toValue: 0, - useNativeDriver: true, - }).start(); - } - }, - }) - ).current; + const handleLogout = () => { + console.log("User logout"); + router.replace("/"); + setShowLogoutAlert(false); + }; return ( <> + {/* Header */} Profile {id} - {/* Overlay Gelap */} - {isDrawerOpen && ( - - - - )} + {/* Drawer Komponen Eksternal */} + + + - {/* Custom Bottom Drawer */} - {isDrawerOpen && ( - - - - { - alert("Pilihan 1 diklik"); - closeDrawer(); - }} - > - Menu Item 1 - - - { - alert("Pilihan 2 diklik"); - closeDrawer(); - }} - > - Menu Item 2 - - - setShowLogoutAlert(true)} - > - Logout Custom - - - - Alert.alert( - "Konfirmasi Logout", - "Apakah Anda sudah yakin?", - [ - { - text: "Batal", - onPress: () => console.log("Batal logout"), - style: "cancel", - }, - { - text: "Ya", - onPress: () => { - console.log("Logout dipilih"); - // Di sini Anda bisa tambahkan fungsi logout seperti clear token, redirect, dll - closeDrawer(); - }, - }, - ], - { cancelable: true } - ) - } - > - Keluar - - - )} - - {showLogoutAlert && ( - - - Konfirmasi Logout - Apakah Anda sudah yakin? - - setShowLogoutAlert(false)} - > - Batal - - { - console.log("Logout dipilih"); - setShowLogoutAlert(false); - closeDrawer(); - }} - > - Ya - - - - - )} + {/* Alert Komponen Eksternal */} + setShowLogoutAlert(false)} + onRightPress={handleLogout} + title="Apakah anda yakin ingin keluar?" + textLeft="Batal" + textRight="Keluar" + colorRight={MainColor.red} + /> ); } - -const stylesDrawer = StyleSheet.create({ - overlay: { - position: "absolute", - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: "black", - opacity: 0.4, - }, - drawer: { - position: "absolute", - left: 0, - right: 0, - bottom: 0, - height: DRAWER_HEIGHT, - backgroundColor: AccentColor.darkblue, - borderTopLeftRadius: 20, - borderTopRightRadius: 20, - padding: 20, - shadowColor: "#000", - shadowOffset: { width: 0, height: -2 }, - shadowOpacity: 0.2, - elevation: 5, - }, - headerBar: { - width: 40, - height: 5, - backgroundColor: MainColor.white, - borderRadius: 5, - alignSelf: "center", - marginVertical: 10, - }, - menuItem: { - padding: 15, - }, -}); - -const styles = StyleSheet.create({ - alertOverlay: { - position: "absolute", - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: "rgba(0,0,0,0.5)", - justifyContent: "center", - alignItems: "center", - zIndex: 999, - }, - alertBox: { - width: "80%", - backgroundColor: "white", - borderRadius: 10, - padding: 20, - alignItems: "center", - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.25, - elevation: 5, - }, - alertTitle: { - fontSize: 18, - fontWeight: "bold", - marginBottom: 10, - }, - alertMessage: { - textAlign: "center", - marginBottom: 20, - }, - alertButtons: { - flexDirection: "row", - justifyContent: "space-between", - width: "100%", - }, - alertButton: { - flex: 1, - padding: 10, - borderRadius: 5, - marginHorizontal: 5, - alignItems: "center", - }, - cancelButton: { - backgroundColor: "#ccc", - }, - confirmButton: { - backgroundColor: "red", - }, - buttonText: { - color: "white", - fontWeight: "bold", - }, -}); diff --git a/app/(application)/profile/edit.tsx b/app/(application)/profile/edit.tsx new file mode 100644 index 0000000..09259ce --- /dev/null +++ b/app/(application)/profile/edit.tsx @@ -0,0 +1,9 @@ +import { Text, View } from "react-native"; + +export default function ProfileEdit() { + return ( + + Profile Edit + + ) +} \ No newline at end of file diff --git a/app/(application)/profile/update-background/[id].tsx b/app/(application)/profile/update-background/[id].tsx new file mode 100644 index 0000000..cab1900 --- /dev/null +++ b/app/(application)/profile/update-background/[id].tsx @@ -0,0 +1,11 @@ +import { Text, View } from "react-native"; +import { useLocalSearchParams } from "expo-router"; + +export default function UpdatePhotoBackground() { + const { id } = useLocalSearchParams(); + return ( + + Update Photo Background {id} + + ) +} \ No newline at end of file diff --git a/app/(application)/profile/update-photo/[id].tsx b/app/(application)/profile/update-photo/[id].tsx new file mode 100644 index 0000000..1342743 --- /dev/null +++ b/app/(application)/profile/update-photo/[id].tsx @@ -0,0 +1,11 @@ +import { Text, View } from "react-native"; +import { useLocalSearchParams } from "expo-router"; + +export default function UpdatePhotoProfile() { + const { id } = useLocalSearchParams(); + return ( + + Update Photo Profile {id} + + ); +} diff --git a/components/Alert/AlertCustom.tsx b/components/Alert/AlertCustom.tsx new file mode 100644 index 0000000..d6732e4 --- /dev/null +++ b/components/Alert/AlertCustom.tsx @@ -0,0 +1,127 @@ +import { AccentColor, MainColor } from "@/constants/color-palet"; +import { TEXT_SIZE_LARGE } from "@/constants/constans-value"; +import React from "react"; +import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; + +interface AlertCustomProps { + isVisible: boolean; + onLeftPress: () => void; + onRightPress: () => void; + title?: string; + message?: string; + textLeft?: string; + textRight?: string; + colorLeft?: string; + colorRight?: string; +} + +export default function AlertCustom({ + isVisible, + onLeftPress, + onRightPress, + title, + message, + textLeft, + textRight, + colorLeft, + colorRight, +}: AlertCustomProps) { + if (!isVisible) return null; + + return ( + + + {title && message ? ( + <> + {title} + {message} + + ) : title ? ( + {title} + ) : ( + {message} + )} + + + {textLeft} + + + {textRight} + + + + + ); +} + +const styles = StyleSheet.create({ + overlay: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "rgba(0,0,0,0.5)", + justifyContent: "center", + alignItems: "center", + zIndex: 999, + }, + alertBox: { + width: "90%", + backgroundColor: MainColor.darkblue, + borderColor: AccentColor.blue, + borderWidth: 1, + borderRadius: 10, + padding: 20, + alignItems: "center", + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + elevation: 5, + }, + alertTitle: { + fontSize: TEXT_SIZE_LARGE, + fontWeight: "bold", + marginBottom: 20, + color: MainColor.white, + }, + alertMessage: { + textAlign: "center", + marginBottom: 20, + color: MainColor.white, + }, + alertButtons: { + flexDirection: "row", + justifyContent: "space-between", + width: "100%", + }, + alertButton: { + flex: 1, + padding: 10, + borderRadius: 5, + marginHorizontal: 5, + alignItems: "center", + }, + leftButton: { + backgroundColor: "gray", + }, + rightButton: { + backgroundColor: MainColor.green, + }, + buttonText: { + color: "white", + fontWeight: "bold", + }, +}); diff --git a/components/Drawer/DrawerCustom.tsx b/components/Drawer/DrawerCustom.tsx new file mode 100644 index 0000000..603d38e --- /dev/null +++ b/components/Drawer/DrawerCustom.tsx @@ -0,0 +1,150 @@ +import React, { useRef } from "react"; +import { + Animated, + PanResponder, + StyleSheet, + View +} from "react-native"; + +import { AccentColor, MainColor } from "@/constants/color-palet"; +import { DRAWER_HEIGHT } from "@/constants/constans-value"; + +interface DrawerCustomProps { + children?: React.ReactNode; + height?: number; + isVisible: boolean; + drawerAnim: Animated.Value; + closeDrawer: () => void; + // openLogoutAlert: () => void; +} + +export default function DrawerCustom({ + children, + height, + isVisible, + drawerAnim, + closeDrawer, +}: // openLogoutAlert, +DrawerCustomProps) { + const panResponder = useRef( + PanResponder.create({ + onMoveShouldSetPanResponder: (_, gestureState) => { + return gestureState.dy > 10; // gesek ke bawah + }, + onPanResponderMove: (_, gestureState) => { + const offset = gestureState.dy; + if (offset >= 0 && offset <= DRAWER_HEIGHT) { + drawerAnim.setValue(offset); + } + }, + onPanResponderRelease: (_, gestureState) => { + if (gestureState.dy > 200) { + closeDrawer(); + } else { + Animated.spring(drawerAnim, { + toValue: 0, + useNativeDriver: true, + }).start(); + } + }, + }) + ).current; + + if (!isVisible) return null; + + return ( + <> + {/* Overlay Gelap */} + + + {/* Custom Bottom Drawer */} + + + + {children} + + {/* { + alert("Pilihan 1 diklik"); + closeDrawer(); + }} + > + Menu Item 1 + + + { + alert("Pilihan 2 diklik"); + closeDrawer(); + }} + > + Menu Item 2 + + + + alert("Logout via Alert bawaan")} + > + Keluar + */} + + + ); +} + +const styles = StyleSheet.create({ + overlay: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "black", + opacity: 0.6, + zIndex: 998, + }, + drawer: { + position: "absolute", + left: 0, + right: 0, + bottom: 0, + backgroundColor: AccentColor.darkblue, + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + padding: 20, + shadowColor: "#000", + shadowOffset: { width: 0, height: -2 }, + shadowOpacity: 0.2, + elevation: 5, + zIndex: 999, + }, + headerBar: { + width: 40, + height: 5, + backgroundColor: MainColor.white, + borderRadius: 5, + alignSelf: "center", + marginVertical: 10, + }, + menuItem: { + padding: 15, + }, +}); diff --git a/components/Drawer/MenuDrawerDynamicGird.tsx b/components/Drawer/MenuDrawerDynamicGird.tsx new file mode 100644 index 0000000..ad6c2ce --- /dev/null +++ b/components/Drawer/MenuDrawerDynamicGird.tsx @@ -0,0 +1,57 @@ +import { AccentColor, MainColor } from "@/constants/color-palet"; +import { ICON_SIZE_MEDIUM, TEXT_SIZE_SMALL } from "@/constants/constans-value"; +import { Ionicons } from "@expo/vector-icons"; +import { View, TouchableOpacity, Text, StyleSheet } from "react-native"; + +const MenuDrawerDynamicGrid = ({ data, columns = 3, onPressItem }: any) => { + const numColumns = columns; + + return ( + + {data.map((item: any, index: any) => ( + onPressItem?.(item)} + > + + + + {item.label} + + ))} + + ); +}; + +export default MenuDrawerDynamicGrid; + +const styles = StyleSheet.create({ + container: { + flexDirection: "row", + flexWrap: "wrap", + padding: 0, + }, + itemContainer: { + padding: 10, + alignItems: "center", + }, + iconContainer: { + width: 56, + height: 56, + borderRadius: 28, + backgroundColor: AccentColor.blue, + justifyContent: "center", + alignItems: "center", + }, + label: { + marginTop: 10, + fontSize: TEXT_SIZE_SMALL, + textAlign: "center", + color: MainColor.white, + }, +}); \ No newline at end of file diff --git a/components/_Interface/types.ts b/components/_Interface/types.ts index 12444ac..ecf36f9 100644 --- a/components/_Interface/types.ts +++ b/components/_Interface/types.ts @@ -1,6 +1,6 @@ import { Href } from "expo-router"; -export { ICustomTab, ITabs }; +export { ICustomTab, ITabs, IMenuDrawerItem }; interface ICustomTab { icon: string; @@ -18,3 +18,10 @@ interface ITabs { isActive: boolean; disabled?: boolean; } + +interface IMenuDrawerItem { + icon: string; + label: string; + path?: string; + color?: string; +} diff --git a/constants/constans-value.ts b/constants/constans-value.ts new file mode 100644 index 0000000..166a20d --- /dev/null +++ b/constants/constans-value.ts @@ -0,0 +1,15 @@ +export { + TEXT_SIZE_SMALL, + TEXT_SIZE_MEDIUM, + TEXT_SIZE_LARGE, + ICON_SIZE_SMALL, + ICON_SIZE_MEDIUM, + DRAWER_HEIGHT, +}; + +const TEXT_SIZE_SMALL = 12; +const TEXT_SIZE_MEDIUM = 14; +const TEXT_SIZE_LARGE = 16; +const ICON_SIZE_SMALL = 20; +const ICON_SIZE_MEDIUM = 24; +const DRAWER_HEIGHT = 500; // tinggi drawer5 diff --git a/eas.build.android b/eas.build.android new file mode 100644 index 0000000..2b00de1 --- /dev/null +++ b/eas.build.android @@ -0,0 +1 @@ +eas build --profile preview \ No newline at end of file diff --git a/eas.json b/eas.json index c6600e8..4804edb 100644 --- a/eas.json +++ b/eas.json @@ -1,6 +1,6 @@ { "cli": { - "version": ">= 16.12.0", + "version": ">= 16.10.0", "appVersionSource": "remote" }, "build": { @@ -9,7 +9,10 @@ "distribution": "internal" }, "preview": { - "distribution": "internal" + "distribution": "internal", + "android": { + "buildType": "apk" + } }, "production": { "autoIncrement": true diff --git a/screens/Profile/menuDrawerSection.tsx b/screens/Profile/menuDrawerSection.tsx new file mode 100644 index 0000000..3853011 --- /dev/null +++ b/screens/Profile/menuDrawerSection.tsx @@ -0,0 +1,31 @@ +import { IMenuDrawerItem } from "@/components/_Interface/types"; +import MenuDrawerDynamicGrid from "@/components/Drawer/MenuDrawerDynamicGird"; +import { router } from "expo-router"; + +export default function Profile_MenuDrawerSection({ + drawerItems, + setShowLogoutAlert, +}: { + drawerItems: IMenuDrawerItem[]; + setShowLogoutAlert: (value: boolean) => void; +}) { + const handlePress = (item: IMenuDrawerItem) => { + if (item.label === "Keluar") { + // console.log("Logout clicked"); + setShowLogoutAlert(true); + } else { + router.push(item.path as any); + } + }; + + return ( + <> + {/* Menu Items */} + + + ); +}