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
@@ -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>
);
};
+136
View File
@@ -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>
);
};
+134
View File
@@ -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>
);
};