project setup
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router, usePathname } from "expo-router";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
|
||||
import { useAppSelector } from "@/store/hooks";
|
||||
import { selectIsMobile } from "@/store/screen/screenSizeSlice";
|
||||
import { IoniconsName } from "@/types/index";
|
||||
import { useRoleAccess } from "@/hooks/useRoleAccess";
|
||||
|
||||
const HIDDEN_PATHS = ["/accounts/admins", "/accounts/todos"];
|
||||
|
||||
interface TabItem {
|
||||
label: string;
|
||||
route: string;
|
||||
icon?: IoniconsName;
|
||||
}
|
||||
|
||||
interface TabGroup {
|
||||
basePath: string;
|
||||
tabs: TabItem[];
|
||||
}
|
||||
|
||||
export const ContentTabs = () => {
|
||||
const pathname = usePathname();
|
||||
const isMobile = useAppSelector(selectIsMobile);
|
||||
const { isManager } = useRoleAccess();
|
||||
|
||||
// Hide top tabs when user lacks permission and is on restricted Accounts pages
|
||||
if (!isManager && HIDDEN_PATHS.includes(pathname)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tabGroups: TabGroup[] = [
|
||||
{
|
||||
basePath: "/",
|
||||
tabs: [
|
||||
{
|
||||
label: "Dashboard",
|
||||
route: "/user-dashboard/index",
|
||||
icon: "home",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
basePath: "/accounts",
|
||||
tabs: [
|
||||
{
|
||||
label: "Users",
|
||||
route: "/accounts/users",
|
||||
icon: "person-outline",
|
||||
},
|
||||
...(isManager
|
||||
? [
|
||||
{
|
||||
label: "Admins",
|
||||
route: "/accounts/admins",
|
||||
icon: "settings" as IoniconsName,
|
||||
},
|
||||
{
|
||||
label: "Todos",
|
||||
route: "/accounts/todos",
|
||||
icon: "list" as IoniconsName,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
{
|
||||
basePath: "/logs",
|
||||
tabs: [{ label: "System", route: "/logs/system", icon: "server" }],
|
||||
},
|
||||
{
|
||||
basePath: "/reports",
|
||||
tabs: [{ label: "Inbox", route: "/reports/inbox", icon: "mail" }],
|
||||
},
|
||||
{
|
||||
basePath: "/data-protection",
|
||||
tabs: [
|
||||
{ label: "General", route: "/data-protection/general", icon: "cog" },
|
||||
],
|
||||
},
|
||||
{
|
||||
basePath: "/settings",
|
||||
tabs: [{ label: "General", route: "/settings/general", icon: "cog" }],
|
||||
},
|
||||
];
|
||||
|
||||
let currentSection = "";
|
||||
|
||||
if (
|
||||
pathname.includes("/dashboard") ||
|
||||
pathname === "/(tabs)" ||
|
||||
pathname === "/"
|
||||
) {
|
||||
currentSection = "/";
|
||||
} else if (pathname.includes("/accounts")) {
|
||||
currentSection = "/accounts";
|
||||
} else if (pathname.includes("/settings")) {
|
||||
currentSection = "/settings";
|
||||
} else if (pathname.includes("/logs")) {
|
||||
currentSection = "/logs";
|
||||
} else if (pathname.includes("/reports")) {
|
||||
currentSection = "/reports";
|
||||
} else if (pathname.includes("/data-protection")) {
|
||||
currentSection = "/data-protection";
|
||||
}
|
||||
|
||||
const activeGroup = tabGroups.find(
|
||||
(group) => group.basePath === currentSection
|
||||
);
|
||||
if (!activeGroup || activeGroup.tabs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isTabActive = (tabRoute: string) => {
|
||||
if (tabRoute === "/user-dashboard/index") {
|
||||
if (pathname === "/(tabs)" || pathname === "/") {
|
||||
return true;
|
||||
}
|
||||
return pathname.includes("/user-dashboard");
|
||||
}
|
||||
|
||||
const routeSegments = tabRoute.split("/");
|
||||
const lastSegment = routeSegments[routeSegments.length - 1];
|
||||
|
||||
return pathname.includes(lastSegment);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
className={`bg-primaryPurpleLight ${isMobile ? "px-1" : "px-2"} ${
|
||||
isMobile ? "pt-2 pb-0" : "pt-3"
|
||||
}`}
|
||||
>
|
||||
<View className={`flex-row ${isMobile ? "justify-start gap-3" : ""}`}>
|
||||
{activeGroup.tabs.map((tab, index) => {
|
||||
const active = isTabActive(tab.route);
|
||||
return (
|
||||
<Pressable
|
||||
key={index}
|
||||
onPress={() => router.push(tab.route as any)}
|
||||
className={`relative mx-1 border-b-2 transition-colors duration-200
|
||||
${active ? "border-white" : "border-transparent"}
|
||||
${isMobile ? "px-1" : "px-4"}
|
||||
`}
|
||||
style={{ paddingBottom: isMobile ? 6 : 4 }}
|
||||
>
|
||||
{isMobile ? (
|
||||
<View className="items-center">
|
||||
<Ionicons
|
||||
name={tab.icon as IoniconsName}
|
||||
size={20}
|
||||
color="white"
|
||||
style={{ opacity: active ? 1 : 0.7 }}
|
||||
/>
|
||||
<Text
|
||||
className="text-white text-[10px] mt-0.5"
|
||||
style={{ opacity: active ? 1 : 0.7 }}
|
||||
>
|
||||
{tab.label}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text
|
||||
className={`text-base text-white transition-colors duration-200
|
||||
${active ? "font-medium" : "font-normal"}
|
||||
`}
|
||||
>
|
||||
{tab.label}
|
||||
</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
import { View, Animated } from "react-native";
|
||||
import { useState, useMemo, useEffect, useRef } from "react";
|
||||
import { debounce } from "lodash";
|
||||
import { router, useLocalSearchParams, usePathname } from "expo-router";
|
||||
|
||||
import { SearchBar } from "@/components/ui/SearchBar";
|
||||
import { SearchDropdown } from "@/components/ui/SearchDropdown";
|
||||
import { SizeEnum } from "@/constants/sizeEnum";
|
||||
import { UserButton } from "@/components/auth/UserButton";
|
||||
import { SearchResult } from "@/store/api/searchApi";
|
||||
|
||||
export const HeaderControls = () => {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const params = useLocalSearchParams<{ q?: string }>();
|
||||
const pathname = usePathname();
|
||||
const searchWidth = useRef(new Animated.Value(250)).current;
|
||||
|
||||
const isOnSearchPage =
|
||||
typeof pathname === "string" && pathname.includes("/search");
|
||||
|
||||
const debouncedSearch = useMemo(
|
||||
() =>
|
||||
debounce((query: string) => {
|
||||
if (!query.trim()) return;
|
||||
}, 300),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
debouncedSearch(searchQuery);
|
||||
// Animate width based on whether user is typing
|
||||
// Use spring animation for smoother feel
|
||||
Animated.spring(searchWidth, {
|
||||
toValue: searchQuery.trim() ? 500 : 250,
|
||||
useNativeDriver: false,
|
||||
tension: 100,
|
||||
friction: 10,
|
||||
}).start();
|
||||
return debouncedSearch.cancel;
|
||||
}, [searchQuery, debouncedSearch, searchWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOnSearchPage) return;
|
||||
const q = (params.q ?? "").toString();
|
||||
setSearchQuery(q);
|
||||
}, [isOnSearchPage, params.q]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOnSearchPage) return;
|
||||
const current = (params.q ?? "").toString();
|
||||
if (searchQuery.trim() !== "" && searchQuery !== current) {
|
||||
router.setParams({ q: searchQuery });
|
||||
}
|
||||
}, [isOnSearchPage, searchQuery, params.q]);
|
||||
|
||||
const handleResultSelect = (result: SearchResult) => {
|
||||
const q = result.title || "";
|
||||
const encodedQ = encodeURIComponent(q);
|
||||
|
||||
// Immediately clear search and collapse animation to prevent lag
|
||||
setSearchQuery("");
|
||||
Animated.timing(searchWidth, {
|
||||
toValue: 250,
|
||||
duration: 200,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
|
||||
// Navigate after starting animation
|
||||
switch (result.type) {
|
||||
case "admin":
|
||||
router.push(`/(tabs)/accounts/admins?q=${encodedQ}`);
|
||||
break;
|
||||
case "user":
|
||||
router.push(`/(tabs)/accounts/users?q=${encodedQ}`);
|
||||
break;
|
||||
case "todo":
|
||||
router.push(`/(tabs)/accounts/todos?q=${encodedQ}`);
|
||||
break;
|
||||
default: {
|
||||
router.push(`/(tabs)/search?q=${encodedQ}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewAllResults = (query: string) => {
|
||||
router.push(`/(tabs)/search?q=${encodeURIComponent(query)}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="flex-row items-center justify-end px-4 absolute right-0 top-[6px] h-8">
|
||||
<Animated.View
|
||||
className="relative"
|
||||
style={{ zIndex: 1000, width: searchWidth }}
|
||||
id="search-container"
|
||||
>
|
||||
<SearchBar
|
||||
searchQuery={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
placeholderText="Search"
|
||||
size={SizeEnum.Small}
|
||||
/>
|
||||
<SearchDropdown
|
||||
searchQuery={searchQuery}
|
||||
onResultSelect={handleResultSelect}
|
||||
onViewAllResults={handleViewAllResults}
|
||||
/>
|
||||
</Animated.View>
|
||||
<UserButton />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,307 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { usePathname } from "expo-router";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Animated, Platform, View } from "react-native";
|
||||
|
||||
import { useAppSelector, useAppDispatch } from "@/store/hooks";
|
||||
import {
|
||||
selectIsMobile,
|
||||
setSidebarCollapsed,
|
||||
} from "@/store/screen/screenSizeSlice";
|
||||
import { SignOutButton } from "@/components/auth/SignOutButton";
|
||||
import { useAuthContext } from "@/auth/AuthContext";
|
||||
import { SidebarToggle } from "@/components/features/navigation/SidebarToggle";
|
||||
import { SidebarItem } from "@/components/features/navigation/SidebarItem";
|
||||
|
||||
const SIDEBAR_STATE_KEY = "sidebarCollapsedState";
|
||||
|
||||
const saveCollapsedState = async (isCollapsed: boolean) => {
|
||||
try {
|
||||
if (Platform.OS === "web") {
|
||||
localStorage.setItem(SIDEBAR_STATE_KEY, JSON.stringify(isCollapsed));
|
||||
} else {
|
||||
await AsyncStorage.setItem(
|
||||
SIDEBAR_STATE_KEY,
|
||||
JSON.stringify(isCollapsed)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving sidebar state:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadCollapsedState = async (): Promise<boolean | null> => {
|
||||
try {
|
||||
let value;
|
||||
if (Platform.OS === "web") {
|
||||
value = localStorage.getItem(SIDEBAR_STATE_KEY);
|
||||
} else {
|
||||
value = await AsyncStorage.getItem(SIDEBAR_STATE_KEY);
|
||||
}
|
||||
return value ? JSON.parse(value) : null;
|
||||
} catch (error) {
|
||||
console.error("Error loading sidebar state:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const Sidebar = ({
|
||||
mobileHidden = false,
|
||||
onRequestMobileToggle,
|
||||
}: {
|
||||
mobileHidden?: boolean;
|
||||
onRequestMobileToggle?: () => void;
|
||||
}) => {
|
||||
const { isAuthenticated } = useAuthContext();
|
||||
const pathname = usePathname();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [labelsVisible, setLabelsVisible] = useState(true);
|
||||
const animatedWidth = useRef(new Animated.Value(200)).current;
|
||||
const textOpacity = useRef(new Animated.Value(1)).current;
|
||||
const iconTranslateX = useRef(new Animated.Value(0)).current;
|
||||
const paddingLeft = useRef(new Animated.Value(16)).current; // 0.5rem = 8px
|
||||
const isMobile = useAppSelector(selectIsMobile);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
const loadSavedState = async () => {
|
||||
if (isMobile) {
|
||||
setCollapsed(true);
|
||||
setLabelsVisible(false);
|
||||
textOpacity.setValue(0);
|
||||
iconTranslateX.setValue(12);
|
||||
paddingLeft.setValue(8);
|
||||
Animated.timing(animatedWidth, {
|
||||
toValue: mobileHidden ? 0 : 80,
|
||||
duration: 250,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
return;
|
||||
}
|
||||
const savedState = await loadCollapsedState();
|
||||
if (savedState !== null) {
|
||||
setCollapsed(savedState);
|
||||
dispatch(setSidebarCollapsed(savedState));
|
||||
setLabelsVisible(!savedState);
|
||||
textOpacity.setValue(savedState ? 0 : 1);
|
||||
iconTranslateX.setValue(savedState ? 12 : 0);
|
||||
paddingLeft.setValue(8);
|
||||
Animated.timing(animatedWidth, {
|
||||
toValue: savedState ? 80 : 200,
|
||||
duration: 300,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
}
|
||||
};
|
||||
|
||||
loadSavedState();
|
||||
}, [
|
||||
animatedWidth,
|
||||
textOpacity,
|
||||
iconTranslateX,
|
||||
paddingLeft,
|
||||
isMobile,
|
||||
mobileHidden,
|
||||
dispatch,
|
||||
]);
|
||||
|
||||
const isActive = (path: string) => {
|
||||
if (path === "/user-dashboard" || path === "/dashboard") {
|
||||
return (
|
||||
pathname.includes("/user-dashboard") ||
|
||||
pathname === "/(tabs)" ||
|
||||
pathname === "/"
|
||||
);
|
||||
}
|
||||
|
||||
if (path === "/accounts") {
|
||||
return pathname.includes("/accounts");
|
||||
}
|
||||
|
||||
if (path === "/settings") {
|
||||
return (
|
||||
pathname.startsWith("/settings") ||
|
||||
pathname.startsWith("/(tabs)/settings")
|
||||
);
|
||||
}
|
||||
|
||||
if (path === "/data-protection") {
|
||||
return (
|
||||
pathname.startsWith("/data-protection") ||
|
||||
pathname.startsWith("/(tabs)/data-protection")
|
||||
);
|
||||
}
|
||||
|
||||
return pathname.includes(path);
|
||||
};
|
||||
|
||||
const toggleSidebar = () => {
|
||||
if (isMobile) {
|
||||
onRequestMobileToggle?.();
|
||||
return;
|
||||
}
|
||||
const newCollapsedState = !collapsed;
|
||||
|
||||
if (!newCollapsedState) {
|
||||
setLabelsVisible(true);
|
||||
}
|
||||
|
||||
Animated.parallel([
|
||||
Animated.timing(animatedWidth, {
|
||||
toValue: newCollapsedState ? 80 : 200,
|
||||
duration: 300,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
Animated.timing(textOpacity, {
|
||||
toValue: newCollapsedState ? 0 : 1,
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(iconTranslateX, {
|
||||
toValue: newCollapsedState ? 12 : 0,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(paddingLeft, {
|
||||
toValue: 8,
|
||||
duration: 300,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
]).start(() => {
|
||||
setLabelsVisible(!newCollapsedState);
|
||||
});
|
||||
if (newCollapsedState) {
|
||||
setLabelsVisible(true);
|
||||
}
|
||||
|
||||
setCollapsed(newCollapsedState);
|
||||
dispatch(setSidebarCollapsed(newCollapsedState));
|
||||
saveCollapsedState(newCollapsedState);
|
||||
};
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
className="border-r px-2"
|
||||
style={{
|
||||
width: animatedWidth,
|
||||
backgroundColor: "#49385D",
|
||||
borderRightColor: "#49385D",
|
||||
borderRightWidth: undefined,
|
||||
paddingHorizontal: undefined,
|
||||
flex: isMobile ? undefined : 1,
|
||||
height: "100%",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<View className="flex-1 flex-col">
|
||||
{!(isMobile && mobileHidden) && (
|
||||
<View
|
||||
className="border-b-2 border-white"
|
||||
style={{
|
||||
paddingBottom: isMobile ? 6 : 0,
|
||||
paddingTop: isMobile ? 6 : 0,
|
||||
}}
|
||||
>
|
||||
<SidebarToggle
|
||||
onPress={toggleSidebar}
|
||||
textOpacity={textOpacity}
|
||||
iconTranslateX={iconTranslateX}
|
||||
showLabel={labelsVisible}
|
||||
className="h-[40px] w-full justify-start"
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{!(isMobile && mobileHidden) && (
|
||||
<View>
|
||||
<SidebarItem
|
||||
paddingLeft={paddingLeft}
|
||||
routerPushValue={"/dashboard/user-dashboard"}
|
||||
isMobile={isMobile}
|
||||
isActive={isActive}
|
||||
iconTranslateX={iconTranslateX}
|
||||
textOpacity={textOpacity}
|
||||
showLabel={labelsVisible}
|
||||
path="/dashboard/user-dashboard"
|
||||
icon="home-outline"
|
||||
label="Home"
|
||||
isFirst
|
||||
/>
|
||||
<SidebarItem
|
||||
paddingLeft={paddingLeft}
|
||||
routerPushValue={"/(tabs)/accounts/users"}
|
||||
isMobile={isMobile}
|
||||
isActive={isActive}
|
||||
iconTranslateX={iconTranslateX}
|
||||
textOpacity={textOpacity}
|
||||
showLabel={labelsVisible}
|
||||
path="/accounts"
|
||||
icon="person-outline"
|
||||
label="Accounts"
|
||||
/>
|
||||
<SidebarItem
|
||||
paddingLeft={paddingLeft}
|
||||
routerPushValue={"/(tabs)/logs/system"}
|
||||
isMobile={isMobile}
|
||||
isActive={isActive}
|
||||
iconTranslateX={iconTranslateX}
|
||||
textOpacity={textOpacity}
|
||||
showLabel={labelsVisible}
|
||||
path="/logs"
|
||||
icon="checkbox-outline"
|
||||
label="Logs"
|
||||
/>
|
||||
<SidebarItem
|
||||
paddingLeft={paddingLeft}
|
||||
routerPushValue={"/(tabs)/reports/inbox"}
|
||||
isMobile={isMobile}
|
||||
isActive={isActive}
|
||||
iconTranslateX={iconTranslateX}
|
||||
textOpacity={textOpacity}
|
||||
showLabel={labelsVisible}
|
||||
path="/reports"
|
||||
icon="copy-outline"
|
||||
label="Reports"
|
||||
/>
|
||||
<SidebarItem
|
||||
paddingLeft={paddingLeft}
|
||||
routerPushValue={"/(tabs)/data-protection/general"}
|
||||
isMobile={isMobile}
|
||||
isActive={isActive}
|
||||
iconTranslateX={iconTranslateX}
|
||||
textOpacity={textOpacity}
|
||||
showLabel={labelsVisible}
|
||||
path="/data-protection"
|
||||
icon="lock-closed-outline"
|
||||
label="Data Protection"
|
||||
/>
|
||||
<SidebarItem
|
||||
paddingLeft={paddingLeft}
|
||||
routerPushValue={"/(tabs)/settings/general"}
|
||||
isMobile={isMobile}
|
||||
isActive={isActive}
|
||||
iconTranslateX={iconTranslateX}
|
||||
textOpacity={textOpacity}
|
||||
showLabel={labelsVisible}
|
||||
path="/settings"
|
||||
icon="settings-outline"
|
||||
label="Settings"
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{!(isMobile && mobileHidden) && (
|
||||
<View className="mb-4 mt-auto px-2">
|
||||
{isAuthenticated && (
|
||||
<SignOutButton
|
||||
textOpacity={textOpacity}
|
||||
iconTranslateX={iconTranslateX}
|
||||
paddingLeft={paddingLeft}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Animated, Pressable, Text } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router } from "expo-router";
|
||||
|
||||
import { IoniconsName } from "@/types/index";
|
||||
|
||||
interface SidebarItemProps {
|
||||
paddingLeft: Animated.Value;
|
||||
routerPushValue: any;
|
||||
isMobile: boolean;
|
||||
isActive: (path: string) => boolean;
|
||||
iconTranslateX: Animated.Value;
|
||||
textOpacity: Animated.Value;
|
||||
path: string;
|
||||
icon: IoniconsName;
|
||||
label: string;
|
||||
isFirst?: boolean;
|
||||
showLabel?: boolean;
|
||||
}
|
||||
|
||||
export const SidebarItem = ({
|
||||
paddingLeft,
|
||||
routerPushValue,
|
||||
isMobile,
|
||||
isActive,
|
||||
iconTranslateX,
|
||||
textOpacity,
|
||||
path,
|
||||
icon,
|
||||
label,
|
||||
isFirst,
|
||||
showLabel = true,
|
||||
}: SidebarItemProps) => {
|
||||
return (
|
||||
<Animated.View
|
||||
style={{
|
||||
paddingLeft,
|
||||
paddingRight: 8,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
onPress={() => router.push(routerPushValue)}
|
||||
className={`flex-row items-center py-3 rounded-lg w-full ${
|
||||
isFirst && !isMobile ? "mt-4" : isMobile ? "mt-1" : ""
|
||||
} ${showLabel ? "justify-start" : "justify-center"}`}
|
||||
style={{
|
||||
backgroundColor: isActive(path) ? "#5A4570" : "transparent",
|
||||
}}
|
||||
>
|
||||
<Animated.View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
...(showLabel
|
||||
? { paddingLeft: 8, transform: [{ translateX: iconTranslateX }] }
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<Ionicons name={icon as IoniconsName} size={24} color="white" />
|
||||
</Animated.View>
|
||||
{showLabel && (
|
||||
<Animated.View
|
||||
style={{
|
||||
opacity: textOpacity,
|
||||
marginLeft: 12,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className={`${
|
||||
isActive(path) ? "text-white font-medium" : "text-gray-300"
|
||||
}`}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Pressable, Text, Animated } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
|
||||
interface SidebarToggleProps {
|
||||
textOpacity?: Animated.Value;
|
||||
iconTranslateX?: Animated.Value;
|
||||
onPress: () => void;
|
||||
showLabel?: boolean;
|
||||
size?: number;
|
||||
color?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SidebarToggle = ({
|
||||
textOpacity,
|
||||
iconTranslateX,
|
||||
onPress,
|
||||
showLabel = false,
|
||||
size = 24,
|
||||
color = "white",
|
||||
className = "",
|
||||
}: SidebarToggleProps) => {
|
||||
return (
|
||||
<Animated.View
|
||||
style={{
|
||||
paddingRight: showLabel ? 4 : 8,
|
||||
paddingLeft: showLabel ? 16 : 8,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
className={`flex-row items-center rounded-lg ${className}`}
|
||||
>
|
||||
<Animated.View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
...(showLabel && iconTranslateX !== undefined
|
||||
? { transform: [{ translateX: iconTranslateX }] }
|
||||
: { width: "100%" }),
|
||||
}}
|
||||
>
|
||||
<Ionicons name="menu" size={size} color={color} />
|
||||
</Animated.View>
|
||||
{showLabel && (
|
||||
<Animated.View
|
||||
style={{
|
||||
...(textOpacity !== undefined ? { opacity: textOpacity } : {}),
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Text className="text-white text-lg ml-2" numberOfLines={1}>
|
||||
Options
|
||||
</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user