project setup

This commit is contained in:
Sone
2025-11-02 10:08:56 +01:00
commit 568838003e
174 changed files with 27100 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
import { Ionicons } from "@expo/vector-icons";
import { Redirect, Tabs, usePathname, router } from "expo-router";
import { Pressable, View } from "react-native";
import { useState } from "react";
import { useAuthContext } from "@/auth/AuthContext";
import { ContentTabs } from "@/components/features/navigation/ContentTabs";
import { Sidebar } from "@/components/features/navigation/Sidebar";
import { SidebarToggle } from "@/components/features/navigation/SidebarToggle";
import { useAppSelector } from "@/store/hooks";
import { selectIsMobile } from "@/store/screen/screenSizeSlice";
import { HeaderControls } from "@/components/features/navigation/HeaderControls";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { UserButton } from "@/components/auth/UserButton";
const TabsLayout = () => {
const isMobile = useAppSelector(selectIsMobile);
const { isAuthenticated, bootstrapping } = useAuthContext();
const pathname = usePathname();
const [mobileHidden, setMobileHidden] = useState(true);
if (bootstrapping) {
return (
<View className="flex-1 bg-primaryPurpleLight justify-center items-center">
<LoadingSpinner />
</View>
);
}
if (!isAuthenticated) {
if (pathname !== "/sign-in") {
return <Redirect href="/sign-in" />;
}
return null;
}
return (
<View className="flex-1 flex-row">
<View
style={{
flexShrink: 0,
}}
>
<Sidebar
mobileHidden={isMobile ? mobileHidden : false}
onRequestMobileToggle={() => setMobileHidden((v) => !v)}
/>
</View>
<View className="flex-1 bg-primaryPurpleLight">
<View className="relative">
<View style={{ paddingLeft: isMobile && mobileHidden ? 80 : 0 }}>
<ContentTabs />
</View>
{isMobile && mobileHidden && (
<View
className="absolute z-50"
style={{
left: 0,
top: 0,
height: 56,
width: 80,
backgroundColor: "transparent",
borderBottomColor: "transparent",
borderBottomWidth: 0,
paddingLeft: 4,
paddingRight: 4,
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
}}
>
<SidebarToggle
onPress={() => setMobileHidden(false)}
showLabel={!mobileHidden}
/>
</View>
)}
{isMobile ? (
<View className="absolute flex flex-row items-center justify-center z-50 right-4 top-2 space-x-3">
<Pressable className="p-1" onPress={() => router.push("/search")}>
<Ionicons name="search" size={20} color="white" />
</Pressable>
<UserButton />
</View>
) : (
<HeaderControls />
)}
</View>
<Tabs
screenOptions={{
headerShown: false,
tabBarStyle: { display: "none" },
}}
>
<Tabs.Screen name="index" />
<Tabs.Screen name="accounts/index" />
<Tabs.Screen name="logs/index" />
<Tabs.Screen name="reports/index" />
<Tabs.Screen name="data-protection/index" />
<Tabs.Screen name="settings/index" />
<Tabs.Screen name="search" />
</Tabs>
</View>
</View>
);
};
export default TabsLayout;
+7
View File
@@ -0,0 +1,7 @@
import { AdminDetailsPage } from "@/components/features/admins/AdminDetailsPage";
const AdminDetails = () => {
return <AdminDetailsPage />;
};
export default AdminDetails;
+11
View File
@@ -0,0 +1,11 @@
import { AdminsPage } from "@/components/features/admins/AdminsPage";
import { useRoleAccess } from "@/hooks/useRoleAccess";
import { AccessDenied } from "@/components/features/auth/AccessDenied";
const Admins = () => {
const { isManager } = useRoleAccess();
if (!isManager) return <AccessDenied />;
return <AdminsPage />;
};
export default Admins;
+7
View File
@@ -0,0 +1,7 @@
import { Redirect } from "expo-router";
const Accounts = () => {
return <Redirect href="/accounts/users" />;
};
export default Accounts;
+11
View File
@@ -0,0 +1,11 @@
import { TodosPage } from "@/components/features/todos/TodosPage";
import { useRoleAccess } from "@/hooks/useRoleAccess";
import { AccessDenied } from "@/components/features/auth/AccessDenied";
const Todos = () => {
const { isManager } = useRoleAccess();
if (!isManager) return <AccessDenied />;
return <TodosPage />;
};
export default Todos;
+7
View File
@@ -0,0 +1,7 @@
import { UserDetailsPage } from "@/components/features/users/UserDetailsPage";
const UserDetails = () => {
return <UserDetailsPage />;
};
export default UserDetails;
+7
View File
@@ -0,0 +1,7 @@
import { UsersPage } from "@/components/features/users/UsersPage";
const Users = () => {
return <UsersPage />;
};
export default Users;
+7
View File
@@ -0,0 +1,7 @@
import { Redirect } from "expo-router";
const Dashboard = () => {
return <Redirect href="/dashboard/user-dashboard" />;
};
export default Dashboard;
@@ -0,0 +1,7 @@
import UserDashboardPage from "@/components/features/dashboard/UserDashboardPage";
const UserDashboard = () => {
return <UserDashboardPage />;
};
export default UserDashboard;
@@ -0,0 +1,28 @@
import { Text, View } from "react-native";
import { useAppSelector } from "@/store/hooks";
import { selectIsMobile } from "@/store/screen/screenSizeSlice";
const General = () => {
const isMobile = useAppSelector(selectIsMobile);
return (
<View
className={`flex-1 p-1 ${isMobile ? "p-1" : "p-5"} bg-primaryPurpleLight`}
>
<Text
className={`text-2xl font-bold ${
isMobile ? "mb-1" : "mb-4"
} text-white`}
>
General Security
</Text>
<Text className="text-base text-gray-600">
This is the General Security tab content. It would typically show
application errors, exceptions, and critical issues that need attention.
</Text>
</View>
);
};
export default General;
+7
View File
@@ -0,0 +1,7 @@
import { Redirect } from "expo-router";
const DataProtection = () => {
return <Redirect href="/data-protection/general" />;
};
export default DataProtection;
+3
View File
@@ -0,0 +1,3 @@
import Dashboard from "./dashboard";
export default Dashboard;
+7
View File
@@ -0,0 +1,7 @@
import { Redirect } from "expo-router";
const Logs = () => {
return <Redirect href="/logs/system" />;
};
export default Logs;
+7
View File
@@ -0,0 +1,7 @@
import { LogsPage } from "@/components/features/logs/LogsPage";
const SystemLogs = () => {
return <LogsPage />;
};
export default SystemLogs;
+28
View File
@@ -0,0 +1,28 @@
import { Text, View } from "react-native";
import { useAppSelector } from "@/store/hooks";
import { selectIsMobile } from "@/store/screen/screenSizeSlice";
const Inbox = () => {
const isMobile = useAppSelector(selectIsMobile);
return (
<View
className={`flex-1 p-1 ${isMobile ? "p-1" : "p-5"} bg-primaryPurpleLight`}
>
<Text
className={`text-2xl font-bold ${
isMobile ? "mb-1" : "mb-4"
} text-white`}
>
Inbox
</Text>
<Text className="text-base text-gray-600">
This is the Inbox tab content. It would typically show application
errors, exceptions, and critical issues that need attention.
</Text>
</View>
);
};
export default Inbox;
+7
View File
@@ -0,0 +1,7 @@
import { Redirect } from "expo-router";
const Reports = () => {
return <Redirect href="/reports/inbox" />;
};
export default Reports;
+140
View File
@@ -0,0 +1,140 @@
import { useEffect, useState, useCallback, useMemo } from "react";
import { View, Text, FlatList } from "react-native";
import { useLocalSearchParams, router } from "expo-router";
import { ActivityIndicator } from "react-native-paper";
import { useAppSelector } from "@/store/hooks";
import { selectIsMobile } from "@/store/screen/screenSizeSlice";
import { SearchResult, useGlobalSearchQuery } from "@/store/api/searchApi";
import { SearchBar } from "@/components/ui/SearchBar";
import { SizeEnum } from "@/constants/sizeEnum";
import { ResultRow } from "@/components/features/search/ResultRow";
import { normalizeEnumLike } from "@/utils/formattingUtils";
import { useDebounce } from "@/hooks/useDebounce";
import { Button } from "@/components/ui/Button";
const SearchPage = () => {
const params = useLocalSearchParams<{ q?: string }>();
const [q, setQ] = useState(params.q ?? "");
const isMobile = useAppSelector(selectIsMobile);
useEffect(() => {
setQ(params.q ?? "");
}, [params.q]);
const debouncedQ = useDebounce(q, 300);
const [limit] = useState(100);
const { data, isFetching, isLoading, isError } = useGlobalSearchQuery(
{ q: debouncedQ, page: 1, limit },
{
skip: !debouncedQ || debouncedQ.trim().length < 2,
refetchOnMountOrArgChange: true,
refetchOnFocus: true,
}
);
const results = useMemo(() => data?.results ?? [], [data?.results]);
const [expandedIds, setExpandedIds] = useState<Record<string, boolean>>({});
const toggleExpanded = useCallback((id: string) => {
setExpandedIds((prev) => ({ ...prev, [id]: !prev[id] }));
}, []);
const isExpanded = useCallback(
(id: string) => !!expandedIds[id],
[expandedIds]
);
// Reset expansions when new query results arrive
useEffect(() => {
if (data?.results) setExpandedIds({});
}, [debouncedQ, data?.results]);
// Update route param only after debounce
useEffect(() => {
router.setParams({ q: debouncedQ || "" });
}, [debouncedQ]);
// Preprocess heavy formatting outside render
const formattedResults = useMemo(
() =>
results.map((item) => ({
...item,
subtitle: item.subtitle ? normalizeEnumLike(item.subtitle) : null,
description: item.description
? normalizeEnumLike(item.description)
: null,
createdAtFormatted: item.createdAt
? new Date(item.createdAt).toLocaleString()
: null,
})),
[results]
);
const renderResult = useCallback(
({ item }: { item: SearchResult }) => (
<ResultRow
item={item}
expanded={isExpanded(item.id)}
onToggle={toggleExpanded}
/>
),
[isExpanded, toggleExpanded]
);
return (
<View className="flex-1 p-4">
<View>
{isMobile && (
<Button
onPress={() => router.push("/dashboard/user-dashboard")}
disabled={isFetching}
className="mb-2"
>
Go to User Dashboard
</Button>
)}
<SearchBar
searchQuery={q}
onChange={setQ}
placeholderText="Search"
size={SizeEnum.Regular}
/>
</View>
{!q || q.trim().length < 2 ? (
<View className="mt-6">
<Text className="text-gray-600">Start typing to search.</Text>
</View>
) : isLoading && results.length === 0 ? (
<View className="mt-6">
<ActivityIndicator />
</View>
) : isError && results.length === 0 ? (
<View className="mt-6">
<Text className="text-red-500">Search failed. Please try again.</Text>
</View>
) : (
<View className="flex-1">
<Text className="text-gray-700 mb-2 mt-2">
Results: {results.length}
</Text>
<FlatList
data={formattedResults as SearchResult[]}
keyExtractor={(item) => `${item.type}-${item.id}`}
renderItem={renderResult}
contentContainerStyle={{ paddingBottom: 16 }}
ItemSeparatorComponent={() => <View style={{ height: 12 }} />}
/>
{isFetching && (
<View className="my-3">
<ActivityIndicator />
</View>
)}
</View>
)}
</View>
);
};
export default SearchPage;
+29
View File
@@ -0,0 +1,29 @@
import { Text, View } from "react-native";
import { useAppSelector } from "@/store/hooks";
import { selectIsMobile } from "@/store/screen/screenSizeSlice";
const GeneralSettings = () => {
const isMobile = useAppSelector(selectIsMobile);
return (
<View
className={`flex-1 p-1 ${isMobile ? "p-1" : "p-5"} bg-primaryPurpleLight`}
>
<Text
className={`text-2xl font-bold ${
isMobile ? "mb-1" : "mb-4"
} text-white`}
>
General Settings
</Text>
<Text className="text-base text-gray-600">
This is the General Settings tab content. It would typically show
application preferences, language settings, and other general
configuration options.
</Text>
</View>
);
};
export default GeneralSettings;
+7
View File
@@ -0,0 +1,7 @@
import { Redirect } from "expo-router";
const Settings = () => {
return <Redirect href="/settings/general" />;
};
export default Settings;