project setup
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import { DataTable, Text } from "react-native-paper";
|
||||
import { View } from "react-native";
|
||||
|
||||
import { UserLoginLog } from "@/store/api/usersApi";
|
||||
import { formatDate, formatTime } from "@/utils/dateUtils";
|
||||
import { LogsTableHeader } from "@/components/features/logs/LogsTableHeader";
|
||||
import { TableSortDirection } from "@/constants/tableSortDirectionEnum";
|
||||
|
||||
interface DesktopLogsTableProps {
|
||||
logs: UserLoginLog[];
|
||||
sortColumn: string | null;
|
||||
sortDirection: TableSortDirection;
|
||||
onSort: (column: string) => void;
|
||||
}
|
||||
|
||||
export const DesktopLogsTable = ({
|
||||
logs,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
onSort,
|
||||
}: DesktopLogsTableProps) => {
|
||||
return (
|
||||
<DataTable>
|
||||
<LogsTableHeader
|
||||
sortColumn={sortColumn}
|
||||
sortDirection={sortDirection}
|
||||
onSort={onSort}
|
||||
/>
|
||||
|
||||
{logs.map((log) => (
|
||||
<DataTable.Row key={log.id}>
|
||||
<DataTable.Cell>
|
||||
<View className="flex-row items-center flex-shrink pr-2">
|
||||
<Text numberOfLines={1}>{log.name}</Text>
|
||||
</View>
|
||||
</DataTable.Cell>
|
||||
<DataTable.Cell>
|
||||
<View className="flex-row items-center flex-shrink pr-2">
|
||||
<Text numberOfLines={1}>{log.email}</Text>
|
||||
</View>
|
||||
</DataTable.Cell>
|
||||
<DataTable.Cell>
|
||||
<View className="flex-row items-center flex-shrink pr-2">
|
||||
<Text numberOfLines={1}>
|
||||
{formatDate(log.lastLoginAt)} {formatTime(log.lastLoginAt)}
|
||||
</Text>
|
||||
</View>
|
||||
</DataTable.Cell>
|
||||
<DataTable.Cell>
|
||||
<View className="flex-row items-center flex-shrink pr-2">
|
||||
<Text numberOfLines={1}>{log.activity}</Text>
|
||||
</View>
|
||||
</DataTable.Cell>
|
||||
</DataTable.Row>
|
||||
))}
|
||||
</DataTable>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,136 @@
|
||||
import {
|
||||
useDeferredValue,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { View, Text } from "react-native";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
|
||||
import { LogsTable } from "@/components/features/logs/LogsTable";
|
||||
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
|
||||
import { NoData } from "@/components/ui/NoData";
|
||||
import { useGetUserLogsQuery } from "@/store/api/usersApi";
|
||||
import { TableSortDirection } from "@/constants/tableSortDirectionEnum";
|
||||
import { useAppSelector } from "@/store/hooks";
|
||||
import { selectIsMobile } from "@/store/screen/screenSizeSlice";
|
||||
|
||||
export const LogsPage = () => {
|
||||
const [page, setPage] = useState(0);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(25);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
||||
const [sortDirection, setSortDirection] = useState<TableSortDirection>(
|
||||
TableSortDirection.ASCENDING
|
||||
);
|
||||
const isMobile = useAppSelector(selectIsMobile);
|
||||
|
||||
const deferredSearch = useDeferredValue(searchQuery);
|
||||
const [debouncedSearch, setDebouncedSearch] = useState(deferredSearch);
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(
|
||||
() => setDebouncedSearch(deferredSearch.trim()),
|
||||
450
|
||||
);
|
||||
return () => clearTimeout(handler);
|
||||
}, [deferredSearch]);
|
||||
|
||||
const queryArgs = useMemo(() => {
|
||||
const searchParam =
|
||||
debouncedSearch && debouncedSearch.length >= 2
|
||||
? debouncedSearch
|
||||
: undefined;
|
||||
return {
|
||||
page: page + 1,
|
||||
limit: itemsPerPage,
|
||||
search: searchParam,
|
||||
} as const;
|
||||
}, [page, itemsPerPage, debouncedSearch]);
|
||||
|
||||
const {
|
||||
data: logsData,
|
||||
isLoading,
|
||||
isFetching,
|
||||
error,
|
||||
refetch,
|
||||
} = useGetUserLogsQuery(queryArgs);
|
||||
|
||||
const logs = logsData?.items ?? [];
|
||||
const totalItems = logsData?.total ?? 0;
|
||||
const from = logs.length ? page * itemsPerPage + 1 : 0;
|
||||
const to = logs.length ? Math.min(from + logs.length - 1, totalItems) : 0;
|
||||
|
||||
// Refetch whenever the screen/page gains focus
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
refetch();
|
||||
}, [refetch])
|
||||
);
|
||||
|
||||
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`}
|
||||
>
|
||||
Users login activity
|
||||
</Text>
|
||||
<View
|
||||
className={`bg-lightGray rounded-lg ${isMobile ? "p-1" : "p-5"} flex-1`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<View className="flex-1 justify-center items-center py-10">
|
||||
<LoadingSpinner />
|
||||
<Text className="mt-2">Loading logs...</Text>
|
||||
</View>
|
||||
) : error ? (
|
||||
<View className="flex-1 justify-center items-center py-10">
|
||||
<NoData
|
||||
message="Error loading logs"
|
||||
onRetry={refetch}
|
||||
isRetrying={isFetching}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<LogsTable
|
||||
logs={logs}
|
||||
page={page}
|
||||
itemsPerPage={itemsPerPage}
|
||||
totalItems={totalItems}
|
||||
from={from}
|
||||
to={to}
|
||||
searchQuery={searchQuery}
|
||||
onSearch={(q) => {
|
||||
setSearchQuery(q);
|
||||
setPage(0);
|
||||
}}
|
||||
sortColumn={sortColumn}
|
||||
sortDirection={sortDirection}
|
||||
onSort={(column) => {
|
||||
if (sortColumn === column) {
|
||||
setSortDirection((d) =>
|
||||
d === TableSortDirection.ASCENDING
|
||||
? TableSortDirection.DESCENDING
|
||||
: TableSortDirection.ASCENDING
|
||||
);
|
||||
} else {
|
||||
setSortColumn(column);
|
||||
setSortDirection(TableSortDirection.ASCENDING);
|
||||
}
|
||||
setPage(0);
|
||||
}}
|
||||
onItemsPerPageChange={(value) => {
|
||||
setItemsPerPage(value);
|
||||
setPage(0);
|
||||
}}
|
||||
onPageChange={(newPage) => setPage(newPage)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { View, Text, ScrollView, GestureResponderEvent } from "react-native";
|
||||
|
||||
import { LogsTableControls } from "@/components/features/logs/LogsTableControls";
|
||||
import { TablePagination } from "@/components/shared/TablePagination";
|
||||
import { UserLoginLog } from "@/store/api/usersApi";
|
||||
import { useAppSelector } from "@/store/hooks";
|
||||
import { selectIsMobile } from "@/store/screen/screenSizeSlice";
|
||||
import { DesktopLogsTable } from "@/components/features/logs/DesktopLogsTable";
|
||||
import { MobileLogsTable } from "@/components/features/logs/MobileLogsTable";
|
||||
import { TableSortDirection } from "@/constants/tableSortDirectionEnum";
|
||||
|
||||
interface LogsTableProps {
|
||||
logs: UserLoginLog[];
|
||||
page: number;
|
||||
itemsPerPage: number;
|
||||
totalItems: number;
|
||||
from: number;
|
||||
to: number;
|
||||
searchQuery: string;
|
||||
onSearch: (q: string) => void;
|
||||
onItemsPerPageChange: (value: number) => void;
|
||||
onPageChange: (page: number) => void;
|
||||
sortColumn: string | null;
|
||||
sortDirection: TableSortDirection;
|
||||
onSort: (column: string) => void;
|
||||
}
|
||||
|
||||
export const LogsTable = ({
|
||||
logs,
|
||||
page,
|
||||
itemsPerPage,
|
||||
totalItems,
|
||||
from,
|
||||
to,
|
||||
searchQuery,
|
||||
onSearch,
|
||||
onItemsPerPageChange,
|
||||
onPageChange,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
onSort,
|
||||
}: LogsTableProps) => {
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
const [menuAnchor, setMenuAnchor] = useState<{ x: number; y: number }>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const isMobile = useAppSelector(selectIsMobile);
|
||||
|
||||
const handleItemsPerPageButtonPress = (event: GestureResponderEvent) => {
|
||||
const { pageX, pageY } = event.nativeEvent;
|
||||
setMenuAnchor({ x: pageX, y: pageY });
|
||||
setMenuVisible(true);
|
||||
};
|
||||
|
||||
const sortedLogs = useMemo(() => {
|
||||
if (!sortColumn) return logs;
|
||||
const dir = sortDirection === TableSortDirection.DESCENDING ? -1 : 1;
|
||||
const safe = (v?: string) => (v ?? "").toString().toLowerCase();
|
||||
const out = [...logs];
|
||||
out.sort((a, b) => {
|
||||
let comp = 0;
|
||||
switch (sortColumn) {
|
||||
case "lastLoginAt": {
|
||||
const at = new Date(a.lastLoginAt).getTime();
|
||||
const bt = new Date(b.lastLoginAt).getTime();
|
||||
comp = at - bt;
|
||||
break;
|
||||
}
|
||||
case "name":
|
||||
comp = safe(a.name).localeCompare(safe(b.name));
|
||||
break;
|
||||
case "email":
|
||||
comp = safe(a.email).localeCompare(safe(b.email));
|
||||
break;
|
||||
case "activity":
|
||||
comp = safe(a.activity).localeCompare(safe(b.activity));
|
||||
break;
|
||||
default:
|
||||
comp = 0;
|
||||
}
|
||||
return comp * dir;
|
||||
});
|
||||
return out;
|
||||
}, [logs, sortColumn, sortDirection]);
|
||||
|
||||
return (
|
||||
<View className="flex-1">
|
||||
<LogsTableControls
|
||||
searchQuery={searchQuery}
|
||||
menuVisible={menuVisible}
|
||||
menuAnchor={menuAnchor}
|
||||
onSearch={(q) => onSearch(q)}
|
||||
onMenuDismiss={() => setMenuVisible(false)}
|
||||
onItemsPerPageChange={onItemsPerPageChange}
|
||||
itemsPerPage={itemsPerPage}
|
||||
/>
|
||||
|
||||
{logs.length === 0 ? (
|
||||
<View
|
||||
className={`flex-1 items-center justify-center ${
|
||||
isMobile ? "p-1" : "p-5"
|
||||
}`}
|
||||
>
|
||||
<Text className="text-lg text-gray-500">
|
||||
No logs match your search{searchQuery && ` for "${searchQuery}"`}
|
||||
</Text>
|
||||
</View>
|
||||
) : isMobile ? (
|
||||
<MobileLogsTable logs={sortedLogs} />
|
||||
) : (
|
||||
<ScrollView className="flex-1">
|
||||
<DesktopLogsTable
|
||||
sortColumn={sortColumn}
|
||||
sortDirection={sortDirection}
|
||||
onSort={onSort}
|
||||
logs={sortedLogs}
|
||||
/>
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
<TablePagination
|
||||
page={page}
|
||||
itemsPerPage={itemsPerPage}
|
||||
totalItems={totalItems}
|
||||
from={from}
|
||||
to={to}
|
||||
onPageChange={onPageChange}
|
||||
onItemsPerPageButtonPress={handleItemsPerPageButtonPress}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { View } from "react-native";
|
||||
|
||||
import { TableControls } from "@/components/shared/TableControls";
|
||||
|
||||
interface LogsTableControlsProps {
|
||||
searchQuery: string;
|
||||
menuVisible: boolean;
|
||||
menuAnchor: { x: number; y: number };
|
||||
onSearch: (query: string) => void;
|
||||
onMenuDismiss: () => void;
|
||||
onItemsPerPageChange: (value: number) => void;
|
||||
itemsPerPage?: number;
|
||||
itemsPerPageMenuOptions?: number[];
|
||||
}
|
||||
|
||||
export const LogsTableControls = ({
|
||||
searchQuery,
|
||||
menuVisible,
|
||||
menuAnchor,
|
||||
onSearch,
|
||||
onMenuDismiss,
|
||||
onItemsPerPageChange,
|
||||
itemsPerPage = 25,
|
||||
itemsPerPageMenuOptions,
|
||||
}: LogsTableControlsProps) => {
|
||||
return (
|
||||
<View>
|
||||
<TableControls
|
||||
searchQuery={searchQuery}
|
||||
menuVisible={menuVisible}
|
||||
menuAnchor={menuAnchor}
|
||||
searchPlaceholder="Search logs"
|
||||
onSearch={onSearch}
|
||||
onMenuDismiss={onMenuDismiss}
|
||||
onItemsPerPageChange={onItemsPerPageChange}
|
||||
itemsPerPage={itemsPerPage}
|
||||
itemsPerPageMenuOptions={itemsPerPageMenuOptions || [25, 50, 100]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { DataTable } from "react-native-paper";
|
||||
|
||||
import { SortableHeader } from "@/components/shared/SortableHeader";
|
||||
import { TableSortDirection } from "@/constants/tableSortDirectionEnum";
|
||||
|
||||
interface LogsTableHeaderProps {
|
||||
sortColumn: string | null;
|
||||
sortDirection: TableSortDirection;
|
||||
onSort: (column: string) => void;
|
||||
}
|
||||
|
||||
export const LogsTableHeader = ({
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
onSort,
|
||||
}: LogsTableHeaderProps) => {
|
||||
return (
|
||||
<DataTable.Header>
|
||||
<SortableHeader
|
||||
title="Name"
|
||||
column="name"
|
||||
sortColumn={sortColumn}
|
||||
sortDirection={sortDirection}
|
||||
onSort={onSort}
|
||||
/>
|
||||
<SortableHeader
|
||||
title="Email"
|
||||
column="email"
|
||||
sortColumn={sortColumn}
|
||||
sortDirection={sortDirection}
|
||||
onSort={onSort}
|
||||
/>
|
||||
<SortableHeader
|
||||
title="Time"
|
||||
column="lastLoginAt"
|
||||
sortColumn={sortColumn}
|
||||
sortDirection={sortDirection}
|
||||
onSort={onSort}
|
||||
/>
|
||||
<SortableHeader
|
||||
title="Login activity"
|
||||
column="activity"
|
||||
sortColumn={sortColumn}
|
||||
sortDirection={sortDirection}
|
||||
onSort={onSort}
|
||||
/>
|
||||
</DataTable.Header>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ScrollView, Text, View } from "react-native";
|
||||
|
||||
import { UserLoginLog } from "@/store/api/usersApi";
|
||||
import { formatDate, formatTime } from "@/utils/dateUtils";
|
||||
|
||||
interface MobileLogsTableProps {
|
||||
logs: UserLoginLog[];
|
||||
}
|
||||
|
||||
export const MobileLogsTable = ({ logs }: MobileLogsTableProps) => {
|
||||
return (
|
||||
<View className="flex-1">
|
||||
<ScrollView
|
||||
className="flex-1"
|
||||
contentContainerStyle={{ paddingBottom: 16 }}
|
||||
>
|
||||
{logs.map((log) => (
|
||||
<View key={log.id} className="mb-3 p-4 bg-white rounded-md shadow-sm">
|
||||
<Text className="text-base font-semibold">
|
||||
{log.name}{" "}
|
||||
<Text className="font-normal text-gray-600">({log.email})</Text>
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-700 mt-1">
|
||||
Time: {formatDate(log.lastLoginAt)} {formatTime(log.lastLoginAt)}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-700 mt-1">
|
||||
Login activity: {log.activity}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user