Compare commits

...

3 Commits

Author SHA1 Message Date
c4c16f19c1 Admin
Add:"
- app/(application)/admin/

## NO Issue
2025-08-05 17:40:46 +08:00
48c34aa26c Admin
Add:
- app/(application)/(admin)/

## No Issue
2025-08-05 12:11:25 +08:00
e16b0c2fce Donation
Add:
- [transaction]/success.tsx
- [transaction]/failed.tsx

Fix:
- InformationBox : props type di tambah React.ReactNode
- /donation/create && /donation/create-story : fix route

### No Issue
2025-08-05 11:01:41 +08:00
23 changed files with 1086 additions and 23 deletions

View File

@@ -477,6 +477,20 @@ export default function UserLayout() {
),
}}
/>
<Stack.Screen
name="donation/[id]/(transaction-flow)/[transaction]/success"
options={{
title: "Donasi Berhasil",
headerLeft: () => <BackButton />,
}}
/>
<Stack.Screen
name="donation/[id]/(transaction-flow)/[transaction]/failed"
options={{
title: "Donasi Gagal",
headerLeft: () => <BackButton />,
}}
/>
{/* ========== End Donation Section ========= */}

View File

@@ -5,9 +5,10 @@ import {
Grid,
StackCustom,
TextCustom,
ViewWrapper
ViewWrapper,
} from "@/components";
import { dummyMasterStatusTransaction } from "@/lib/dummy-data/_master/status-transaction";
import { router } from "expo-router";
import { View } from "react-native";
export default function DonationMyDonation() {
@@ -17,6 +18,19 @@ export default function DonationMyDonation() {
);
return dummyMasterStatusTransaction[randomIndex];
});
const handlePress = (value: string) => {
if (value === "menunggu") {
router.push(`/donation/${value}/(transaction-flow)/123/invoice`);
} else if (value === "proses") {
router.push(`/donation/${value}/(transaction-flow)/123/process`);
} else if (value === "berhasil") {
router.push(`/donation/${value}/(transaction-flow)/123/success`);
} else if (value === "gagal") {
router.push(`/donation/${value}/(transaction-flow)/123/failed`);
}
};
return (
<ViewWrapper hideFooter>
{randomStatusData.map((item, index) => (
@@ -24,11 +38,13 @@ export default function DonationMyDonation() {
key={index}
paddingTop={7}
paddingBottom={7}
href={`/investment/${index}`}
onPress={() => {
handlePress(item.value);
}}
>
<Grid>
<Grid.Col span={5}>
<DummyLandscapeImage height={100} />
<DummyLandscapeImage height={100} unClickPath />
</Grid.Col>
<Grid.Col span={1}>
<View />

View File

@@ -0,0 +1,83 @@
import {
BaseBox,
Grid,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { GStyles } from "@/styles/global-styles";
import { FontAwesome6 } from "@expo/vector-icons";
import dayjs from "dayjs";
export default function DonasiFailed() {
return (
<ViewWrapper>
<StackCustom>
<BaseBox>
<StackCustom>
<TextCustom bold align="center">
Transaksi anda gagal karena bukti transfer tidak sesuai dengan
data kami. Jika ini masalah khusus silahkan hubungi pada kontak
whatsapp kami.
</TextCustom>
<FontAwesome6
name="whatsapp"
size={50}
color={MainColor.green}
style={GStyles.alignSelfCenter}
/>
</StackCustom>
</BaseBox>
<BaseBox>
<TextCustom bold align="center" size="large">
Detail Transaksi
</TextCustom>
<Spacing />
<StackCustom>
{listData.map((item, i) => (
<Grid key={i}>
<Grid.Col span={5}>
<TextCustom bold>{item.label}</TextCustom>
</Grid.Col>
<Grid.Col span={7}>
<TextCustom style={{ paddingLeft: 10 }}>
{item.value}
</TextCustom>
</Grid.Col>
</Grid>
))}
</StackCustom>
</BaseBox>
</StackCustom>
</ViewWrapper>
);
}
const listData = [
{
label: "Bank",
value: " BCA",
},
{
label: "Rekening Penerima",
value: "Himpunan Pengusaha Muda Indonesia",
},
{
label: "No Rekening",
value: "2304235678854332",
},
{
label: "Jumlah Donasi",
value: "Rp. 750.000",
},
{
label: "Tanggal",
value: `${dayjs(new Date()).format("DD/MM/YYYY")}`,
},
];

View File

@@ -13,12 +13,12 @@ import { MainColor } from "@/constants/color-palet";
import { router, useLocalSearchParams } from "expo-router";
export default function DonationInvoice() {
const { id } = useLocalSearchParams();
const { id, transaction } = useLocalSearchParams();
return (
<>
<ViewWrapper>
<StackCustom>
<InformationBox text="Mohon transfer ke rekening dibawah" />
<InformationBox text={`Mohon transfer donasi anda ke rekening dibawah dengan Id: ${transaction}`} />
<BaseBox>
<StackCustom gap={"xs"}>
<TextCustom>Nama BANK</TextCustom>

View File

@@ -0,0 +1,83 @@
import {
BaseBox,
Grid,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { GStyles } from "@/styles/global-styles";
import { FontAwesome6 } from "@expo/vector-icons";
import dayjs from "dayjs";
export default function DonationSuccess() {
return (
<ViewWrapper>
<StackCustom>
<BaseBox>
<StackCustom>
<FontAwesome6
name="money-bill-wave"
size={100}
color={MainColor.green}
style={GStyles.alignSelfCenter}
/>
<TextCustom bold align="center">
Terimakasih telah percaya pada kami untuk mengelola dana anda!
Info mengenai update Penggalian Dana ini bisa di lihat di kolom
berita.
</TextCustom>
</StackCustom>
</BaseBox>
<BaseBox>
<TextCustom bold align="center" size="large">
Detail Transaksi
</TextCustom>
<Spacing />
<StackCustom>
{listData.map((item, i) => (
<Grid key={i}>
<Grid.Col span={5}>
<TextCustom bold>{item.label}</TextCustom>
</Grid.Col>
<Grid.Col span={7}>
<TextCustom style={{ paddingLeft: 10 }}>
{item.value}
</TextCustom>
</Grid.Col>
</Grid>
))}
</StackCustom>
</BaseBox>
</StackCustom>
</ViewWrapper>
);
}
const listData = [
{
label: "Bank",
value: " BCA",
},
{
label: "Rekening Penerima",
value: "Himpunan Pengusaha Muda Indonesia",
},
{
label: "No Rekening",
value: "2304235678854332",
},
{
label: "Jumlah Donasi",
value: "Rp. 750.000",
},
{
label: "Tanggal",
value: `${dayjs(new Date()).format("DD/MM/YYYY")}`,
},
];

View File

@@ -57,7 +57,7 @@ export default function DonationCreateStory() {
<Spacing />
<ButtonCustom
onPress={() => {
router.navigate(`/donation/(tabs)/status`);
router.replace(`/donation/(tabs)/status`);
}}
>
Simpan

View File

@@ -66,7 +66,7 @@ export default function DonationCreate() {
<Spacing />
<ButtonCustom
onPress={() => {
router.push("/donation/create-story");
router.replace("/donation/create-story");
}}
>
Selanjutnya

View File

@@ -7,6 +7,8 @@ export default function ApplicationLayout() {
<>
<Stack screenOptions={HeaderStyles}>
<Stack.Screen name="(user)" options={{ headerShown: false }} />
<Stack.Screen name="admin" options={{ headerShown: false }} />
{/* Take Picture */}
<Stack.Screen

View File

@@ -0,0 +1,94 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
ClickableCustom,
DrawerCustom,
StackCustom,
TextCustom,
} from "@/components";
import DrawerAdmin from "@/components/Drawer/DrawerAdmin";
import NavbarMenu from "@/components/Drawer/NavbarMenu";
import SidebarMenu from "@/components/Drawer/SidebarMenu";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { GStyles } from "@/styles/global-styles";
import { FontAwesome6, Ionicons } from "@expo/vector-icons";
import { router, Stack } from "expo-router";
import { useState } from "react";
import { View } from "react-native";
export default function AdminLayout() {
const [openDrawer, setOpenDrawer] = useState(false);
return (
<>
<Stack
screenOptions={{
headerStyle: GStyles.headerStyle,
headerTitleStyle: GStyles.headerTitleStyle,
headerTitleAlign: "center",
contentStyle: {
borderBottomColor: AccentColor.blue,
},
headerLeft: () => (
<Ionicons
name="menu"
size={ICON_SIZE_SMALL}
color={MainColor.white}
onPress={() => setOpenDrawer(true)}
/>
),
headerRight: () => (
<FontAwesome6
name="circle-user"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
),
}}
>
<Stack.Screen name="dashboard" options={{ title: "Dashboard" }} />
<Stack.Screen name="maps" options={{ title: "Maps" }} />
<Stack.Screen name="information" options={{ title: "Information" }} />
<Stack.Screen name="job/index" options={{ title: "Job" }} />
<Stack.Screen name="job/publish" options={{ title: "Job Publish" }} />
<Stack.Screen name="job/review" options={{ title: "Job Review" }} />
<Stack.Screen name="job/reject" options={{ title: "Job Reject" }} />
</Stack>
<DrawerAdmin isVisible={openDrawer} onClose={() => setOpenDrawer(false)}>
<StackCustom gap={"lg"}>
<Ionicons
name="close"
size={ICON_SIZE_SMALL}
color={MainColor.white}
onPress={() => setOpenDrawer(false)}
style={{ alignSelf: "flex-end" }}
/>
<NavbarMenu
items={[
{ label: "Dashboard", icon: "home", link: "/admin/dashboard" },
{ label: "Maps", icon: "map", link: "/admin/maps" },
{
label: "Information",
icon: "information-circle",
link: "/admin/information",
},
{
label: "Job",
icon: "calendar",
links: [
{ label: "Summary", link: "/admin/job" },
{ label: "Publish", link: "/admin/job/publish" },
{ label: "Review", link: "/admin/job/review" },
{ label: "Reject", link: "/admin/job/reject" },
],
},
]}
onClose={() => setOpenDrawer(false)}
/>
</StackCustom>
</DrawerAdmin>
</>
);
}

View File

@@ -0,0 +1,11 @@
import { TextCustom, ViewWrapper } from "@/components";
export default function AdminDashboard() {
return (
<>
<ViewWrapper>
<TextCustom>Admin Dashboard</TextCustom>
</ViewWrapper>
</>
);
}

View File

@@ -0,0 +1,11 @@
import { TextCustom, ViewWrapper } from "@/components";
export default function AdminInformation() {
return (
<>
<ViewWrapper>
<TextCustom>Information</TextCustom>
</ViewWrapper>
</>
);
}

View File

@@ -0,0 +1,11 @@
import { TextCustom, ViewWrapper } from "@/components";
export default function AdminJob() {
return (
<>
<ViewWrapper>
<TextCustom>Admin Job</TextCustom>
</ViewWrapper>
</>
);
}

View File

@@ -0,0 +1,11 @@
import { TextCustom, ViewWrapper } from "@/components";
export default function AdminJobPublish() {
return (
<>
<ViewWrapper>
<TextCustom>Admin Job Publish</TextCustom>
</ViewWrapper>
</>
);
}

View File

@@ -0,0 +1,11 @@
import { TextCustom, ViewWrapper } from "@/components";
export default function AdminJobReject() {
return (
<>
<ViewWrapper>
<TextCustom>Admin Job Reject</TextCustom>
</ViewWrapper>
</>
);
}

View File

@@ -0,0 +1,11 @@
import { TextCustom, ViewWrapper } from "@/components";
export default function AdminJobReview() {
return (
<>
<ViewWrapper>
<TextCustom>Admin Job Review</TextCustom>
</ViewWrapper>
</>
);
}

View File

@@ -0,0 +1,11 @@
import { TextCustom, ViewWrapper } from "@/components";
export default function AdminMaps() {
return (
<>
<ViewWrapper>
<TextCustom>Maps</TextCustom>
</ViewWrapper>
</>
);
}

View File

@@ -3,8 +3,9 @@ import { Ionicons } from "@expo/vector-icons";
import Grid from "../Grid/GridCustom";
import TextCustom from "../Text/TextCustom";
import BaseBox from "./BaseBox";
import React from "react";
export default function InformationBox({ text }: { text: string }) {
export default function InformationBox({ text }: { text: React.ReactNode | string }) {
return (
<>
<BaseBox paddingTop={5} paddingBottom={5}>

View File

@@ -0,0 +1,151 @@
import React, { useEffect, useRef } from "react";
import {
Animated,
Dimensions,
InteractionManager,
PanResponder,
StyleSheet
} from "react-native";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { SafeAreaView } from "react-native-safe-area-context";
// Lebar drawer (bisa di-pass sebagai prop)
const DRAWER_WIDTH = Dimensions.get("window").width * 0.8; // 80% lebar layar
interface DrawerAdminProps {
children?: React.ReactNode;
width?: number; // lebar drawer
isVisible: boolean;
onClose: () => void; // ganti nama dari closeDrawer agar lebih jelas
}
export default function DrawerAdmin({
children,
width = DRAWER_WIDTH,
isVisible,
onClose,
}: DrawerAdminProps) {
const drawerAnim = useRef(new Animated.Value(-width)).current; // mulai dari kiri (tersembunyi)
// Efek untuk handle animasi saat isVisible berubah
useEffect(() => {
if (isVisible) {
Animated.timing(drawerAnim, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}).start();
} else {
Animated.timing(drawerAnim, {
toValue: -width,
duration: 300,
useNativeDriver: true,
}).start();
}
}, [isVisible, width, onClose, drawerAnim]);
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: (_, gestureState) => {
return Math.abs(gestureState.dx) > 10; // deteksi gesek horizontal
},
onPanResponderMove: (_, gestureState) => {
let newAnim = gestureState.dx; // geser ke kanan = dx positif → drawerAnim negatif
newAnim = Math.max(-width, Math.min(0, newAnim)); // batas antara -width dan 0
drawerAnim.setValue(newAnim);
},
onPanResponderRelease: (_, gestureState) => {
if (gestureState.dx > 100) {
// gesek kencang ke kiri → tutup
InteractionManager.runAfterInteractions(() => {
onClose();
});
} else {
// kembali ke posisi terbuka penuh
Animated.spring(drawerAnim, {
toValue: 0,
useNativeDriver: true,
}).start();
}
},
})
).current;
if (!isVisible) return null;
return (
<>
{/* Overlay Gelap */}
<Animated.View
style={[
styles.overlay,
{
opacity: drawerAnim.interpolate({
inputRange: [-width, 0],
outputRange: [0, 0.6],
extrapolate: "clamp",
}),
},
]}
onTouchStart={() => {
InteractionManager.runAfterInteractions(() => {
onClose();
});
}}
/>
{/* Left Drawer */}
<Animated.View
style={[
styles.drawer,
{
width,
transform: [{ translateX: drawerAnim }],
},
]}
{...panResponder.panHandlers}
>
{/* Handle Bar (opsional) */}
{/* <View style={styles.handleBar} /> */}
<SafeAreaView>{children}</SafeAreaView>
</Animated.View>
</>
);
}
const styles = StyleSheet.create({
overlay: {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "black",
zIndex: 998,
},
drawer: {
position: "absolute",
top: 0,
bottom: 0,
left: 0,
backgroundColor: AccentColor.darkblue,
// borderRadius: 20, // opsional
shadowColor: "#000",
shadowOffset: { width: 2, height: 0 },
shadowOpacity: 0.2,
shadowRadius: 5,
elevation: 5,
zIndex: 999,
padding: 20,
},
handleBar: {
width: 10,
height: 5,
backgroundColor: MainColor.yellow,
borderRadius: 5,
alignSelf: "flex-start",
marginBottom: 20,
},
});

View File

@@ -0,0 +1,251 @@
import { AccentColor, MainColor } from "@/constants/color-palet";
import { Ionicons } from "@expo/vector-icons";
import { router, usePathname } from "expo-router";
import React, { useRef, useState } from "react";
import {
Animated,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
import TextCustom from "../Text/TextCustom";
interface NavbarItem {
label: string;
icon?: keyof typeof Ionicons.glyphMap;
color?: string;
link?: string;
links?: {
label: string;
link: string;
}[];
initiallyOpened?: boolean;
}
interface NavbarMenuProps {
items: NavbarItem[];
onClose?: () => void;
}
export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
const pathname = usePathname();
const [activeLink, setActiveLink] = useState<string | null>(null);
const [openKeys, setOpenKeys] = useState<string[]>([]); // Untuk kontrol dropdown
// Toggle dropdown
const toggleOpen = (label: string) => {
setOpenKeys((prev) =>
prev.includes(label) ? prev.filter((key) => key !== label) : [label]
);
};
return (
<View style={styles.container}>
{items.map((item) => (
<MenuItem
key={item.label}
item={item}
onClose={onClose}
activeLink={activeLink}
setActiveLink={setActiveLink}
isOpen={openKeys.includes(item.label)}
toggleOpen={() => toggleOpen(item.label)}
/>
))}
</View>
);
}
// Komponen Item Menu
function MenuItem({
item,
onClose,
activeLink,
setActiveLink,
isOpen,
toggleOpen,
}: {
item: NavbarItem;
onClose?: () => void;
activeLink: string | null;
setActiveLink: (link: string | null) => void;
isOpen: boolean;
toggleOpen: () => void;
}) {
const isActive = activeLink === item.link;
const animatedHeight = useRef(new Animated.Value(0)).current;
// Animasi saat isOpen berubah
React.useEffect(() => {
Animated.timing(animatedHeight, {
toValue: isOpen ? (item.links ? item.links.length * 40 : 0) : 0,
duration: 200,
useNativeDriver: false,
}).start();
}, [isOpen, item.links]);
// Jika ada submenu
if (item.links && item.links.length > 0) {
return (
<View>
{/* Parent Item */}
<TouchableOpacity style={styles.parentItem} onPress={toggleOpen}>
<Ionicons
name={item.icon}
size={20}
color={MainColor.white}
style={styles.icon}
/>
<Text style={styles.parentText}>{item.label}</Text>
<Ionicons
name={isOpen ? "chevron-up" : "chevron-down"}
size={16}
color={MainColor.white}
/>
</TouchableOpacity>
{/* Submenu (Animated) */}
<Animated.View
style={[
styles.submenu,
// {
// backgroundColor: "red",
// },
{
height: animatedHeight,
opacity: animatedHeight.interpolate({
inputRange: [0, item.links.length * 40],
outputRange: [0, 1],
extrapolate: "clamp",
}),
},
]}
>
{item.links.map((subItem, index) => {
const isSubActive = activeLink === subItem.link;
return (
<TouchableOpacity
key={index}
style={[styles.subItem, isSubActive && styles.subItemActive]}
onPress={() => {
setActiveLink(subItem.link);
onClose?.();
router.push(subItem.link as any);
}}
>
<Ionicons
name="caret-forward-sharp"
size={16}
color={isSubActive ? MainColor.yellow : MainColor.white}
style={styles.icon}
/>
<Text
style={[
styles.subText,
isSubActive && { color: MainColor.yellow },
]}
>
{subItem.label}
</Text>
</TouchableOpacity>
);
})}
</Animated.View>
</View>
);
}
// Menu tanpa submenu
return (
<TouchableOpacity
style={[styles.singleItem, isActive && styles.singleItemActive]}
onPress={() => {
setActiveLink(item.link || null);
onClose?.();
router.push(item.link as any);
}}
>
<Ionicons
name={item.icon}
size={20}
color={isActive ? MainColor.yellow : MainColor.white}
style={styles.icon}
/>
<Text
style={[
styles.singleText,
{ color: isActive ? MainColor.yellow : MainColor.white },
]}
>
{item.label}
</Text>
</TouchableOpacity>
);
}
// Styles
const styles = StyleSheet.create({
container: {
marginTop: 20,
},
parentItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 10,
backgroundColor: AccentColor.darkblue,
borderRadius: 8,
marginBottom: 5,
justifyContent: "space-between",
},
parentText: {
flex: 1,
fontSize: 16,
fontWeight: "500",
marginLeft: 10,
color: MainColor.white,
},
singleItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 10,
backgroundColor: AccentColor.darkblue,
borderRadius: 8,
marginBottom: 5,
},
singleItemActive: {
backgroundColor: AccentColor.blue,
},
singleText: {
fontSize: 16,
fontWeight: "500",
marginLeft: 10,
color: MainColor.white,
},
icon: {
width: 24,
textAlign: "center",
paddingRight: 10,
},
submenu: {
overflow: "hidden",
marginLeft: 30,
marginTop: 5,
},
subItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 8,
paddingHorizontal: 10,
borderRadius: 6,
marginBottom: 4,
},
subItemActive: {
backgroundColor: AccentColor.blue,
},
subText: {
color: MainColor.white,
fontSize: 14,
},
});

View File

@@ -0,0 +1,279 @@
import React, { useRef, useState, useEffect } from "react";
import {
Animated,
Dimensions,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { Ionicons } from "@expo/vector-icons";
import { router, usePathname } from "expo-router";
// Lebar sidebar
const SIDEBAR_WIDTH = Dimensions.get("window").width * 0.8;
interface SidebarItem {
label: string;
icon?: keyof typeof Ionicons.glyphMap;
color?: string;
link?: string;
links?: {
label: string;
link: string;
}[];
initiallyOpened?: boolean;
}
interface SidebarMenuProps {
items: SidebarItem[];
onClose?: () => void;
}
export default function SidebarMenu({ items, onClose }: SidebarMenuProps) {
const pathname = usePathname();
const [activeLink, setActiveLink] = useState<string | null>(null);
const [openKeys, setOpenKeys] = useState<string[]>([]); // Untuk kontrol dropdown
// Normalisasi path: hapus trailing slash
const normalizePath = (path: string) => path.replace(/\/+$/, "");
const normalizedPathname = pathname ? normalizePath(pathname) : "";
// Set activeLink saat pathname berubah
useEffect(() => {
if (normalizedPathname) {
setActiveLink(normalizedPathname);
}
}, [normalizedPathname]);
// Auto-buka dropdown jika submenu aktif
useEffect(() => {
const activeItem = items.find((item) =>
item.links?.some((sub) => sub.link === normalizedPathname)
);
if (activeItem && !openKeys.includes(activeItem.label)) {
setOpenKeys([activeItem.label]);
}
}, [normalizedPathname, items, openKeys]);
// Toggle dropdown
const toggleOpen = (label: string) => {
setOpenKeys((prev) =>
prev.includes(label) ? prev.filter((key) => key !== label) : [label]
);
};
return (
<View style={styles.container}>
{items.map((item) => (
<MenuItem
key={item.label}
item={item}
onClose={onClose}
activeLink={activeLink}
setActiveLink={setActiveLink}
isOpen={openKeys.includes(item.label)}
toggleOpen={() => toggleOpen(item.label)}
/>
))}
</View>
);
}
// Komponen Item Menu
function MenuItem({
item,
onClose,
activeLink,
setActiveLink,
isOpen,
toggleOpen,
}: {
item: SidebarItem;
onClose?: () => void;
activeLink: string | null;
setActiveLink: (link: string | null) => void;
isOpen: boolean;
toggleOpen: () => void;
}) {
const animatedHeight = useRef(new Animated.Value(0)).current;
// Animasi saat isOpen berubah
React.useEffect(() => {
Animated.timing(animatedHeight, {
toValue: isOpen ? (item.links ? item.links.length * 40 : 0) : 0,
duration: 200,
useNativeDriver: false,
}).start();
}, [isOpen, item.links]);
// Cek apakah menu ini aktif
const isActive = activeLink === item.link;
// Cek apakah ada submenu aktif
const hasActiveSubItem = item.links?.some((sub) => sub.link === activeLink);
// Jika ada submenu
if (item.links && item.links.length > 0) {
return (
<View>
{/* Parent Item */}
<TouchableOpacity style={styles.parentItem} onPress={toggleOpen}>
<Ionicons
name={item.icon}
size={20}
color={MainColor.white}
style={styles.icon}
/>
<Text style={styles.parentText}>{item.label}</Text>
<Ionicons
name={isOpen ? "chevron-up" : "chevron-down"}
size={16}
color={MainColor.white}
/>
</TouchableOpacity>
{/* Submenu (Animated) */}
<Animated.View
style={[
styles.submenu,
{
height: animatedHeight,
opacity: animatedHeight.interpolate({
inputRange: [0, item.links.length * 40],
outputRange: [0, 1],
extrapolate: "clamp",
}),
},
]}
>
{item.links.map((subItem, index) => {
const isSubActive = activeLink === subItem.link;
return (
<TouchableOpacity
key={index}
style={[styles.subItem, isSubActive && styles.subItemActive]}
onPress={() => {
setActiveLink(subItem.link);
onClose?.();
router.push(subItem.link as any);
}}
>
<Ionicons
name="caret-forward-sharp"
size={16}
color={isSubActive ? MainColor.yellow : MainColor.white}
style={styles.icon}
/>
<Text
style={[
styles.subText,
isSubActive && { color: MainColor.yellow },
]}
>
{subItem.label}
</Text>
</TouchableOpacity>
);
})}
</Animated.View>
</View>
);
}
// Menu tanpa submenu
return (
<TouchableOpacity
style={[styles.singleItem, isActive && styles.singleItemActive]}
onPress={() => {
setActiveLink(item.link || null);
onClose?.();
router.push(item.link as any);
}}
>
<Ionicons
name={item.icon}
size={20}
color={isActive ? MainColor.yellow : MainColor.white}
style={styles.icon}
/>
<Text
style={[
styles.singleText,
{ color: isActive ? MainColor.yellow : MainColor.white },
]}
>
{item.label}
</Text>
</TouchableOpacity>
);
}
// Styles
const styles = StyleSheet.create({
container: {
marginTop: 20,
},
parentItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 10,
backgroundColor: AccentColor.darkblue,
borderRadius: 8,
marginBottom: 5,
justifyContent: "space-between",
},
parentText: {
flex: 1,
fontSize: 16,
fontWeight: "500",
marginLeft: 10,
color: MainColor.white,
},
singleItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 10,
backgroundColor: AccentColor.darkblue,
borderRadius: 8,
marginBottom: 5,
},
singleItemActive: {
backgroundColor: AccentColor.blue,
},
singleText: {
fontSize: 16,
fontWeight: "500",
marginLeft: 10,
color: MainColor.white,
},
icon: {
width: 24,
textAlign: "center",
paddingRight: 10,
},
submenu: {
overflow: "hidden",
marginLeft: 30,
marginTop: 5,
},
subItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 8,
paddingHorizontal: 10,
borderRadius: 6,
marginBottom: 4,
},
subItemActive: {
backgroundColor: AccentColor.blue,
},
subText: {
color: MainColor.white,
fontSize: 14,
},
});

View File

@@ -38,8 +38,9 @@ export default function LoginView() {
// router.navigate(`/(application)/(image)/preview-image/${id}`);
// router.replace("/(application)/(user)/event/(tabs)");
// router.replace("/(application)/coba");
// router.navigate("/investment/(tabs)")
router.navigate("/crowdfunding")
// router.navigate("/investment/(tabs)")1
// router.navigate("/crowdfunding")
router.navigate("/admin/dashboard")
}
return (

View File

@@ -52,11 +52,17 @@ export const drawerItemsProfile = ({
label: "Tambah portofolio",
path: `/(application)/portofolio/${id}/create`,
},
// {
// icon: "settings",
// label: "Dashboard Admin",
// path: `/(application)/profile/dashboard-admin`,
// },
{
icon: (
<Ionicons
name="settings"
size={ICON_SIZE_MEDIUM}
color={AccentColor.white}
/>
),
label: "Dashboard Admin",
path: `/(application)/(admin)/dashboard`,
},
{
icon: (
<Ionicons

View File

@@ -26,9 +26,14 @@ export default function Profile_MenuDrawerSection({
<>
{/* Menu Items */}
<MenuDrawerDynamicGrid
data={drawerItems}
data={drawerItems.map((item) => ({
icon: item.icon,
label: item.label,
path: item.path as any,
color: item.color,
}))}
columns={4} // Ubah ke 2 jika ingin 2 kolom per baris
onPressItem={handlePress}
onPressItem={(item) => handlePress(item as any)}
/>
</>
);