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.Screen name="(user)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(admin)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="admin" options={{ headerShown: false }} />
|
||||
|
||||
|
||||
{/* Take Picture */}
|
||||
<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(`/(application)/(user)/profile/${id}`);
|
||||
router.navigate("/(application)/(user)/home");
|
||||
// router.navigate("/(application)/(user)/home");
|
||||
// router.navigate(`/(application)/profile/${id}/edit`);
|
||||
// router.navigate(`/(application)/(user)/portofolio/${id}`)
|
||||
// router.navigate(`/(application)/(image)/preview-image/${id}`);
|
||||
// router.replace("/(application)/(user)/event/(tabs)");
|
||||
// router.replace("/(application)/coba");
|
||||
// router.navigate("/investment/(tabs)")
|
||||
// router.navigate("/investment/(tabs)")1
|
||||
// router.navigate("/crowdfunding")
|
||||
router.navigate("/admin/dashboard")
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user