@7span/react-list 0.0.5 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ import { memo, useCallback, useMemo } from "react";
2
+ import { useListContext } from "../context/list-provider";
3
+
4
+ export const ReactListLoadMore = memo(({ children }) => {
5
+ const { listState } = useListContext();
6
+ const { data, count, pagination, setPage, loader, error } = listState;
7
+ const { page, perPage } = pagination;
8
+ const { isLoading } = loader;
9
+
10
+ const hasMoreItems = useMemo(
11
+ () => page * perPage < count,
12
+ [page, perPage, count]
13
+ );
14
+
15
+ const loadMore = useCallback(() => {
16
+ if (hasMoreItems && !isLoading) {
17
+ setPage(page + 1);
18
+ }
19
+ }, [hasMoreItems, isLoading, setPage, page]);
20
+
21
+ const scope = useMemo(
22
+ () => ({
23
+ isLoading,
24
+ loadMore,
25
+ hasMoreItems,
26
+ }),
27
+ [isLoading, loadMore, hasMoreItems]
28
+ );
29
+
30
+ if (!data || data.length === 0) {
31
+ return null;
32
+ }
33
+
34
+ if (error) {
35
+ return null;
36
+ }
37
+
38
+ return children(scope);
39
+ });
@@ -0,0 +1,29 @@
1
+ import { memo, useMemo } from "react";
2
+ import { useListContext } from "../context/list-provider";
3
+
4
+ export const ReactListLoader = memo(({ children, position = "overlay" }) => {
5
+ const { listState } = useListContext();
6
+ const { loader } = listState;
7
+ const { isLoading, initializingState } = loader;
8
+
9
+ const scope = useMemo(
10
+ () => ({
11
+ isLoading,
12
+ }),
13
+ [isLoading]
14
+ );
15
+
16
+ if (!initializingState && !isLoading) {
17
+ return null;
18
+ }
19
+
20
+ return (
21
+ <div>
22
+ {children || (
23
+ <div>
24
+ <p>Loading...</p>
25
+ </div>
26
+ )}
27
+ </div>
28
+ );
29
+ });
@@ -0,0 +1,158 @@
1
+ import { memo, useMemo } from "react";
2
+ import { useListContext } from "../context/list-provider";
3
+
4
+ export const ReactListPagination = memo(
5
+ ({
6
+ children,
7
+ pageLinks = 5,
8
+ renderFirst,
9
+ renderPrev,
10
+ renderPages,
11
+ renderPage,
12
+ renderNext,
13
+ renderLast,
14
+ }) => {
15
+ const { listState } = useListContext();
16
+ const { data, count, pagination, setPage, loader, error } = listState;
17
+ const { page, perPage } = pagination;
18
+ const { initialLoading, isLoading } = loader;
19
+
20
+ const paginationState = useMemo(() => {
21
+ const pagesCount = Math.ceil(count / perPage);
22
+ const halfWay = Math.floor(pageLinks / 2);
23
+ const hasNext = page * perPage < count;
24
+ const hasPrev = page !== 1;
25
+ return { pagesCount, halfWay, hasNext, hasPrev };
26
+ }, [count, perPage, page, pageLinks]);
27
+
28
+ const pagesToDisplay = useMemo(() => {
29
+ const { pagesCount, halfWay } = paginationState;
30
+ const pages = Array.from({ length: Math.min(pageLinks, pagesCount) });
31
+
32
+ if (page <= halfWay) {
33
+ return pages.map((_, index) => index + 1);
34
+ } else if (pagesCount - page < halfWay) {
35
+ return pages.map((_, index) => pagesCount - index).reverse();
36
+ } else {
37
+ return pages.map((_, index) => page - halfWay + index);
38
+ }
39
+ }, [page, pageLinks, paginationState]);
40
+
41
+ const navigation = useMemo(
42
+ () => ({
43
+ prev: () => setPage(page - 1),
44
+ next: () => setPage(page + 1),
45
+ first: () => setPage(1),
46
+ last: () => setPage(paginationState.pagesCount),
47
+ setPage: (newPage) => setPage(newPage),
48
+ }),
49
+ [setPage, page, paginationState.pagesCount]
50
+ );
51
+
52
+ const scope = useMemo(
53
+ () => ({
54
+ page,
55
+ perPage,
56
+ count,
57
+ ...paginationState,
58
+ pagesToDisplay,
59
+ ...navigation,
60
+ }),
61
+ [page, perPage, count, paginationState, pagesToDisplay, navigation]
62
+ );
63
+
64
+ if (initialLoading) return null;
65
+
66
+ if (!data || data.length === 0) {
67
+ return null;
68
+ }
69
+
70
+ if (error) {
71
+ return null;
72
+ }
73
+
74
+ if (children) {
75
+ return children(scope);
76
+ }
77
+
78
+ return (
79
+ <div className="react-list-pagination">
80
+ {renderFirst ? (
81
+ renderFirst(scope)
82
+ ) : (
83
+ <button
84
+ type="button"
85
+ disabled={!paginationState.hasPrev}
86
+ onClick={navigation.first}
87
+ >
88
+ First
89
+ </button>
90
+ )}
91
+
92
+ {renderPrev ? (
93
+ renderPrev(scope)
94
+ ) : (
95
+ <button
96
+ type="button"
97
+ disabled={!paginationState.hasPrev}
98
+ onClick={navigation.prev}
99
+ >
100
+ Prev
101
+ </button>
102
+ )}
103
+
104
+ {renderPages ? (
105
+ renderPages(scope)
106
+ ) : (
107
+ <div>
108
+ {pagesToDisplay.map((pageNum) => {
109
+ const isActive = pageNum === page;
110
+ const pageScope = { ...scope, page: pageNum, isActive };
111
+
112
+ return renderPage ? (
113
+ renderPage(pageScope)
114
+ ) : (
115
+ <div key={`page-${pageNum}`}>
116
+ {isActive ? (
117
+ <span>{pageNum}</span>
118
+ ) : (
119
+ <button
120
+ type="button"
121
+ onClick={() => navigation.setPage(pageNum)}
122
+ >
123
+ {pageNum}
124
+ </button>
125
+ )}
126
+ </div>
127
+ );
128
+ })}
129
+ </div>
130
+ )}
131
+
132
+ {renderNext ? (
133
+ renderNext(scope)
134
+ ) : (
135
+ <button
136
+ type="button"
137
+ disabled={!paginationState.hasNext}
138
+ onClick={navigation.next}
139
+ >
140
+ Next
141
+ </button>
142
+ )}
143
+
144
+ {renderLast ? (
145
+ renderLast(scope)
146
+ ) : (
147
+ <button
148
+ type="button"
149
+ disabled={!paginationState.hasNext}
150
+ onClick={navigation.last}
151
+ >
152
+ Last
153
+ </button>
154
+ )}
155
+ </div>
156
+ );
157
+ }
158
+ );
@@ -0,0 +1,65 @@
1
+ import { memo, useCallback, useMemo } from "react";
2
+ import { useListContext } from "../context/list-provider";
3
+
4
+ export const ReactListPerPage = memo(
5
+ ({ children, options = [10, 25, 50, 100] }) => {
6
+ const { listState } = useListContext();
7
+ const { data, pagination, setPerPage, loader, error } = listState;
8
+ const { perPage } = pagination;
9
+ const { initialLoading } = loader;
10
+
11
+ const serializedOptions = useMemo(() => {
12
+ return options.map((item) => {
13
+ if (typeof item !== "object") {
14
+ return {
15
+ value: item,
16
+ label: item,
17
+ };
18
+ }
19
+ return item;
20
+ });
21
+ }, [options]);
22
+
23
+ const handlePerPageChange = useCallback(
24
+ (e) => {
25
+ setPerPage(Number(e.target.value));
26
+ },
27
+ [setPerPage]
28
+ );
29
+
30
+ const scope = useMemo(
31
+ () => ({
32
+ perPage,
33
+ setPerPage,
34
+ options: serializedOptions,
35
+ }),
36
+ [perPage, setPerPage, serializedOptions]
37
+ );
38
+
39
+ if (initialLoading) return null;
40
+
41
+ if (!data || data.length === 0) {
42
+ return null;
43
+ }
44
+
45
+ if (error) {
46
+ return null;
47
+ }
48
+
49
+ return (
50
+ <div className="react-list-per-page">
51
+ {children ? (
52
+ children(scope)
53
+ ) : (
54
+ <select value={perPage} onChange={handlePerPageChange}>
55
+ {serializedOptions.map((option) => (
56
+ <option key={`option-${option.value}`} value={option.value}>
57
+ {option.label} items per page
58
+ </option>
59
+ ))}
60
+ </select>
61
+ )}
62
+ </div>
63
+ );
64
+ }
65
+ );
@@ -0,0 +1,34 @@
1
+ import { memo, useCallback, useMemo } from "react";
2
+ import { useListContext } from "../context/list-provider";
3
+
4
+ export const ReactListRefresh = memo(({ children }) => {
5
+ const { listState } = useListContext();
6
+ const { loader, refresh } = listState;
7
+ const { isLoading, initialLoading } = loader;
8
+
9
+ const handleRefresh = useCallback(() => {
10
+ refresh({ isRefresh: true });
11
+ }, [refresh]);
12
+
13
+ const scope = useMemo(
14
+ () => ({
15
+ isLoading,
16
+ refresh: handleRefresh,
17
+ }),
18
+ [isLoading, handleRefresh]
19
+ );
20
+
21
+ if (initialLoading) return null;
22
+
23
+ if (children) {
24
+ return children(scope);
25
+ }
26
+
27
+ return (
28
+ <div className="react-list-refresh">
29
+ <button onClick={handleRefresh} disabled={isLoading}>
30
+ {isLoading ? "Loading..." : "Refresh"}
31
+ </button>
32
+ </div>
33
+ );
34
+ });
@@ -0,0 +1,59 @@
1
+ import { memo, useEffect, useRef, useState } from "react";
2
+ import { useListContext } from "../context/list-provider";
3
+
4
+ export const ReactListSearch = memo(({ children, debounceTime = 500 }) => {
5
+ const { listState } = useListContext();
6
+ const { search, setSearch } = listState;
7
+ const [localSearch, setLocalSearch] = useState(search ?? "");
8
+ const debounceTimerRef = useRef(null);
9
+
10
+ // Sync local state with context when search prop changes
11
+ useEffect(() => {
12
+ if (search !== localSearch) {
13
+ setLocalSearch(search ?? "");
14
+ }
15
+ }, [search]);
16
+
17
+ const handleChange = (value) => {
18
+ setLocalSearch(value);
19
+
20
+ // Clear any existing timer
21
+ if (debounceTimerRef.current) {
22
+ clearTimeout(debounceTimerRef.current);
23
+ }
24
+
25
+ // Set a new timer
26
+ debounceTimerRef.current = setTimeout(() => {
27
+ setSearch(value);
28
+ }, debounceTime);
29
+ };
30
+
31
+ // Clean up timer on unmount
32
+ useEffect(() => {
33
+ return () => {
34
+ if (debounceTimerRef.current) {
35
+ clearTimeout(debounceTimerRef.current);
36
+ }
37
+ };
38
+ }, []);
39
+
40
+ const scope = {
41
+ search: localSearch,
42
+ setSearch: handleChange,
43
+ };
44
+
45
+ return (
46
+ <div className="react-list-search">
47
+ {children ? (
48
+ children(scope)
49
+ ) : (
50
+ <input
51
+ type="text"
52
+ value={localSearch}
53
+ onChange={(e) => handleChange(e.target.value)}
54
+ placeholder="Search..."
55
+ />
56
+ )}
57
+ </div>
58
+ );
59
+ });
@@ -0,0 +1,51 @@
1
+ import { memo, useMemo } from "react";
2
+ import { useListContext } from "../context/list-provider";
3
+
4
+ export const ReactListSummary = memo(({ children }) => {
5
+ const { listState } = useListContext();
6
+ const { data, count, pagination, loader, error } = listState;
7
+ const { page, perPage } = pagination;
8
+ const { initialLoading, isLoading } = loader;
9
+
10
+ const summaryData = useMemo(() => {
11
+ const from = page * perPage - perPage + 1;
12
+ const to = Math.min(page * perPage, count);
13
+ const visibleCount = data?.length || 0;
14
+
15
+ return { from, to, visibleCount };
16
+ }, [page, perPage, count, data]);
17
+
18
+ const scope = useMemo(
19
+ () => ({
20
+ ...summaryData,
21
+ count,
22
+ }),
23
+ [summaryData, count]
24
+ );
25
+
26
+ if (initialLoading) return null;
27
+
28
+ if (!data || data.length === 0) {
29
+ return null;
30
+ }
31
+
32
+ if (error) {
33
+ return null;
34
+ }
35
+
36
+ return (
37
+ <div className="react-list-summary">
38
+ {children ? (
39
+ children(scope)
40
+ ) : (
41
+ <span>
42
+ Showing <span>{summaryData.visibleCount}</span> items (
43
+ <span>
44
+ {summaryData.from} - {summaryData.to}
45
+ </span>
46
+ ) out of <span>{count}</span>
47
+ </span>
48
+ )}
49
+ </div>
50
+ );
51
+ });
@@ -0,0 +1,42 @@
1
+ export const deepEqual = (obj1, obj2) => {
2
+ if (obj1 === obj2) return true;
3
+
4
+ if (obj1 == null || obj2 == null) return obj1 === obj2;
5
+
6
+ if (typeof obj1 !== "object" || typeof obj2 !== "object")
7
+ return obj1 === obj2;
8
+
9
+ if (Array.isArray(obj1) && Array.isArray(obj2)) {
10
+ if (obj1.length !== obj2.length) return false;
11
+ for (let i = 0; i < obj1.length; i++) {
12
+ if (!deepEqual(obj1[i], obj2[i])) return false;
13
+ }
14
+ return true;
15
+ }
16
+
17
+ if (Array.isArray(obj1) || Array.isArray(obj2)) return false;
18
+
19
+ const keys1 = Object.keys(obj1).filter((key) => obj1[key] !== undefined);
20
+ const keys2 = Object.keys(obj2).filter((key) => obj2[key] !== undefined);
21
+
22
+ if (keys1.length !== keys2.length) return false;
23
+
24
+ for (let key of keys1) {
25
+ if (!keys2.includes(key)) return false;
26
+ if (!deepEqual(obj1[key], obj2[key])) return false;
27
+ }
28
+
29
+ return true;
30
+ };
31
+
32
+ export const hasActiveFilters = (currentFilters, initialFilters) => {
33
+ if (!initialFilters || Object.keys(initialFilters).length === 0) {
34
+ return currentFilters && Object.keys(currentFilters).length > 0;
35
+ }
36
+
37
+ if (!currentFilters || Object.keys(currentFilters).length === 0) {
38
+ return false;
39
+ }
40
+
41
+ return !deepEqual(currentFilters, initialFilters);
42
+ };
@@ -0,0 +1,55 @@
1
+ import { createContext, useContext, useMemo, useState } from "react";
2
+
3
+ const ListContext = createContext(null);
4
+
5
+ export const ReactListProvider = ({ children, config }) => {
6
+ const { requestHandler, stateManager = {} } = config;
7
+ const [listState, setListState] = useState({
8
+ data: [],
9
+ response: null,
10
+ error: null,
11
+ count: 0,
12
+ selection: [],
13
+ pagination: {
14
+ page: 1,
15
+ perPage: 25,
16
+ },
17
+ loader: {
18
+ isLoading: false,
19
+ initialLoading: true,
20
+ },
21
+ sort: {
22
+ sortBy: null,
23
+ sortOrder: "desc",
24
+ },
25
+ search: "",
26
+ filters: {},
27
+ attrs: [],
28
+ isEmpty: true,
29
+ isInitializing: true,
30
+ });
31
+
32
+ if (!requestHandler) {
33
+ throw new Error("ListProvider: requestHandler is required.");
34
+ }
35
+
36
+ const value = useMemo(
37
+ () => ({
38
+ requestHandler,
39
+ stateManager,
40
+ listState,
41
+ setListState,
42
+ }),
43
+ [requestHandler, stateManager, listState]
44
+ );
45
+
46
+ return <ListContext.Provider value={value}>{children}</ListContext.Provider>;
47
+ };
48
+
49
+ export const useListContext = () => {
50
+ const context = useContext(ListContext);
51
+ if (!context) {
52
+ throw new Error("useListContext must be used within a ListProvider");
53
+ }
54
+ return context;
55
+ };
package/src/index.js ADDED
@@ -0,0 +1,15 @@
1
+ export { ReactListAttributes } from "./components/attributes";
2
+ export { ReactListEmpty } from "./components/empty";
3
+ export { ReactListError } from "./components/error";
4
+ export { ReactListGoTo } from "./components/go-to";
5
+ export { ReactListInitialLoader } from "./components/initial-loader";
6
+ export { ReactListItems } from "./components/items";
7
+ export { default } from "./components/list";
8
+ export { ReactListLoadMore } from "./components/load-more";
9
+ export { ReactListLoader } from "./components/loader";
10
+ export { ReactListPagination } from "./components/pagination";
11
+ export { ReactListPerPage } from "./components/per-page";
12
+ export { ReactListRefresh } from "./components/refresh";
13
+ export { ReactListSearch } from "./components/search";
14
+ export { ReactListSummary } from "./components/summary";
15
+ export { ReactListProvider } from "./context/list-provider";