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