Admin
Add:" - app/(application)/admin/ ## NO Issue
This commit is contained in:
@@ -1,43 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
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 { Stack } from "expo-router";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Text } from "react-native";
|
|
||||||
|
|
||||||
export default function AdminDashboard() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Text>Dashboard</Text>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Text } from "react-native";
|
|
||||||
|
|
||||||
export default function AdminInformation() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Text>Information</Text>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Text } from "react-native";
|
|
||||||
|
|
||||||
export default function AdminMaps() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Text>Maps</Text>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,8 @@ export default function ApplicationLayout() {
|
|||||||
<>
|
<>
|
||||||
<Stack screenOptions={HeaderStyles}>
|
<Stack screenOptions={HeaderStyles}>
|
||||||
<Stack.Screen name="(user)" options={{ headerShown: false }} />
|
<Stack.Screen name="(user)" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="(admin)" options={{ headerShown: false }} />
|
<Stack.Screen name="admin" options={{ headerShown: false }} />
|
||||||
|
|
||||||
|
|
||||||
{/* Take Picture */}
|
{/* Take Picture */}
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
|
|||||||
94
app/(application)/admin/_layout.tsx
Normal file
94
app/(application)/admin/_layout.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
11
app/(application)/admin/dashboard.tsx
Normal file
11
app/(application)/admin/dashboard.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { TextCustom, ViewWrapper } from "@/components";
|
||||||
|
|
||||||
|
export default function AdminDashboard() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ViewWrapper>
|
||||||
|
<TextCustom>Admin Dashboard</TextCustom>
|
||||||
|
</ViewWrapper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
app/(application)/admin/information.tsx
Normal file
11
app/(application)/admin/information.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { TextCustom, ViewWrapper } from "@/components";
|
||||||
|
|
||||||
|
export default function AdminInformation() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ViewWrapper>
|
||||||
|
<TextCustom>Information</TextCustom>
|
||||||
|
</ViewWrapper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
app/(application)/admin/job/index.tsx
Normal file
11
app/(application)/admin/job/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { TextCustom, ViewWrapper } from "@/components";
|
||||||
|
|
||||||
|
export default function AdminJob() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ViewWrapper>
|
||||||
|
<TextCustom>Admin Job</TextCustom>
|
||||||
|
</ViewWrapper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
app/(application)/admin/job/publish.tsx
Normal file
11
app/(application)/admin/job/publish.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { TextCustom, ViewWrapper } from "@/components";
|
||||||
|
|
||||||
|
export default function AdminJobPublish() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ViewWrapper>
|
||||||
|
<TextCustom>Admin Job Publish</TextCustom>
|
||||||
|
</ViewWrapper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
app/(application)/admin/job/reject.tsx
Normal file
11
app/(application)/admin/job/reject.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { TextCustom, ViewWrapper } from "@/components";
|
||||||
|
|
||||||
|
export default function AdminJobReject() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ViewWrapper>
|
||||||
|
<TextCustom>Admin Job Reject</TextCustom>
|
||||||
|
</ViewWrapper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
app/(application)/admin/job/review.tsx
Normal file
11
app/(application)/admin/job/review.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { TextCustom, ViewWrapper } from "@/components";
|
||||||
|
|
||||||
|
export default function AdminJobReview() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ViewWrapper>
|
||||||
|
<TextCustom>Admin Job Review</TextCustom>
|
||||||
|
</ViewWrapper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
app/(application)/admin/maps.tsx
Normal file
11
app/(application)/admin/maps.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { TextCustom, ViewWrapper } from "@/components";
|
||||||
|
|
||||||
|
export default function AdminMaps() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ViewWrapper>
|
||||||
|
<TextCustom>Maps</TextCustom>
|
||||||
|
</ViewWrapper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
151
components/Drawer/DrawerAdmin.tsx
Normal file
151
components/Drawer/DrawerAdmin.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
251
components/Drawer/NavbarMenu.tsx
Normal file
251
components/Drawer/NavbarMenu.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
279
components/Drawer/SidebarMenu.tsx
Normal file
279
components/Drawer/SidebarMenu.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -32,14 +32,15 @@ export default function LoginView() {
|
|||||||
|
|
||||||
// router.navigate("/verification");
|
// router.navigate("/verification");
|
||||||
// router.navigate(`/(application)/(user)/profile/${id}`);
|
// router.navigate(`/(application)/(user)/profile/${id}`);
|
||||||
router.navigate("/(application)/(user)/home");
|
// router.navigate("/(application)/(user)/home");
|
||||||
// router.navigate(`/(application)/profile/${id}/edit`);
|
// router.navigate(`/(application)/profile/${id}/edit`);
|
||||||
// router.navigate(`/(application)/(user)/portofolio/${id}`)
|
// router.navigate(`/(application)/(user)/portofolio/${id}`)
|
||||||
// router.navigate(`/(application)/(image)/preview-image/${id}`);
|
// router.navigate(`/(application)/(image)/preview-image/${id}`);
|
||||||
// router.replace("/(application)/(user)/event/(tabs)");
|
// router.replace("/(application)/(user)/event/(tabs)");
|
||||||
// router.replace("/(application)/coba");
|
// router.replace("/(application)/coba");
|
||||||
// router.navigate("/investment/(tabs)")
|
// router.navigate("/investment/(tabs)")1
|
||||||
// router.navigate("/crowdfunding")
|
// router.navigate("/crowdfunding")
|
||||||
|
router.navigate("/admin/dashboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user