next-data-fetcher
Version:
A flexible data fetching system for Next.js applications with real-time updates and pagination
1,189 lines (1,175 loc) • 43.9 kB
JavaScript
// src/core/FetcherRegistry.ts
var FetcherRegistry = class {
constructor() {
this.apiBasePath = "/api/data";
this.baseUrl = typeof process !== "undefined" && process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : typeof window !== "undefined" ? window.location.origin : "http://localhost:3000";
this.fetchers = /* @__PURE__ */ new Map();
}
static getInstance() {
if (!FetcherRegistry.instance) {
FetcherRegistry.instance = new FetcherRegistry();
}
return FetcherRegistry.instance;
}
register(componentId, fetcher) {
this.fetchers.set(componentId, fetcher);
}
getFetcher(componentId) {
return this.fetchers.get(componentId);
}
setApiBasePath(path) {
this.apiBasePath = path;
}
getApiBasePath() {
return this.apiBasePath;
}
setBaseUrl(url) {
this.baseUrl = url.endsWith("/") ? url.slice(0, -1) : url;
}
getBaseUrl() {
return this.baseUrl;
}
// Method to handle URL construction
getDataUrl(componentId, dataSource = "json", isServer = false) {
if (isServer) {
return `${this.baseUrl}${this.apiBasePath}?component=${componentId}&dataSource=${dataSource}`;
}
return `${this.apiBasePath}?component=${componentId}&dataSource=${dataSource}`;
}
};
// src/core/realtime.ts
import { EventEmitter } from "events";
var RealtimeManager = class {
constructor() {
this.emitter = new EventEmitter();
this.emitter.setMaxListeners(100);
this.sseClients = /* @__PURE__ */ new Set();
this.isServerSide = typeof window === "undefined";
}
static getInstance() {
if (!RealtimeManager.instance) {
RealtimeManager.instance = new RealtimeManager();
}
return RealtimeManager.instance;
}
// Subscribe to data changes for a specific component
subscribe(componentId, callback) {
const eventName = `data-change:${componentId}`;
this.emitter.on(eventName, callback);
return () => {
this.emitter.off(eventName, callback);
};
}
// Publish data changes
publish(event) {
const eventName = `data-change:${event.componentId}`;
this.emitter.emit(eventName, event);
if (this.isServerSide) {
this.broadcastToSSEClients(event);
}
}
// Register a new SSE client
registerSSEClient(id, send) {
this.sseClients.add({ id, send });
}
// Unregister an SSE client
unregisterSSEClient(id) {
for (const client of this.sseClients) {
if (client.id === id) {
this.sseClients.delete(client);
break;
}
}
}
// Broadcast to all SSE clients
broadcastToSSEClients(event) {
const data = JSON.stringify(event);
for (const client of this.sseClients) {
try {
client.send(data);
} catch (error) {
console.error(`Error sending to SSE client ${client.id}:`, error);
this.sseClients.delete(client);
}
}
}
};
var realtimeManager = RealtimeManager.getInstance();
// src/core/BaseFetcher.ts
var _BaseFetcher = class {
constructor(options) {
this.baseServerUrl = typeof process !== "undefined" && process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : typeof window !== "undefined" ? window.location.origin : "http://localhost:3000";
// Add a cache for client-side data
this.clientCache = /* @__PURE__ */ new Map();
// Cache expiration time in milliseconds (5 minutes)
this.cacheExpirationTime = 5 * 60 * 1e3;
// Realtime subscription cleanup function
this.unsubscribe = null;
this.options = {
...options,
pagination: options.pagination || {
page: 1,
limit: 0,
// 0 means no pagination
enabled: false
}
};
this.setupRealtimeSubscription();
}
// Set up realtime subscription
setupRealtimeSubscription() {
if (this.unsubscribe) {
this.unsubscribe();
}
this.unsubscribe = realtimeManager.subscribe(this.options.componentId, this.handleDataChange.bind(this));
}
// Handle data change events
handleDataChange(event) {
console.log(`Received data change for ${this.options.componentId}:`, event);
this.invalidateCache();
}
// Invalidate the cache
invalidateCache() {
const cacheKeys = Array.from(this.clientCache.keys());
for (const key of cacheKeys) {
if (key.includes(this.options.componentId)) {
this.clientCache.delete(key);
}
}
if (typeof window === "undefined") {
const serverCacheKeys = Array.from(_BaseFetcher.serverCache.keys());
for (const key of serverCacheKeys) {
if (key.includes(this.options.componentId)) {
_BaseFetcher.serverCache.delete(key);
}
}
}
}
getUrl(isServer) {
var _a;
const registry = FetcherRegistry.getInstance();
if (this.options.endpoint && this.options.dataSource === "api") {
return this.options.endpoint;
}
let url = registry.getDataUrl(this.options.componentId, this.options.dataSource, isServer);
if (((_a = this.options.pagination) == null ? void 0 : _a.enabled) && this.options.pagination.limit > 0) {
url += `&page=${this.options.pagination.page}&limit=${this.options.pagination.limit}`;
}
return url;
}
async fetchJsonData(isServer) {
var _a, _b, _c;
const paginationString = ((_a = this.options.pagination) == null ? void 0 : _a.enabled) ? `_page${this.options.pagination.page}_limit${this.options.pagination.limit}` : "";
const cacheKey = `json_${this.options.componentId}${paginationString}`;
if (isServer) {
if (_BaseFetcher.serverCache.has(cacheKey)) {
console.log(`Using cached data for ${this.options.componentId} from server cache`);
return { data: _BaseFetcher.serverCache.get(cacheKey) };
}
} else {
const cachedData = this.clientCache.get(cacheKey);
if (cachedData && Date.now() - cachedData.timestamp < this.cacheExpirationTime) {
console.log(`Using cached data for ${this.options.componentId} from client cache`);
return {
data: cachedData.data,
totalItems: cachedData.totalItems,
totalPages: cachedData.totalPages
};
}
}
const url = this.getUrl(isServer);
try {
const response = await fetch(url, {
// Add cache: 'no-store' to prevent caching issues when switching sources
cache: "no-store"
});
if (!response.ok) {
throw new Error(`Failed to fetch data: ${response.statusText}`);
}
const responseData = await response.json();
let parsedData;
let totalItems;
let totalPages;
if (responseData.data && Array.isArray(responseData.data)) {
parsedData = this.parseData(responseData.data);
totalItems = (_b = responseData.pagination) == null ? void 0 : _b.totalItems;
totalPages = (_c = responseData.pagination) == null ? void 0 : _c.totalPages;
} else if (Array.isArray(responseData)) {
parsedData = this.parseData(responseData);
} else {
console.warn("Unexpected response format:", responseData);
parsedData = [];
}
if (isServer) {
_BaseFetcher.serverCache.set(cacheKey, parsedData);
} else {
this.clientCache.set(cacheKey, {
data: parsedData,
timestamp: Date.now(),
totalItems,
totalPages
});
}
return { data: parsedData, totalItems, totalPages };
} catch (error) {
console.error("Error fetching JSON data:", error);
throw error;
}
}
async fetchCsvData(isServer) {
const cacheKey = `csv_${this.options.componentId}`;
if (isServer) {
if (_BaseFetcher.serverCache.has(cacheKey)) {
console.log(`Using cached data for ${this.options.componentId} from server cache`);
return { data: _BaseFetcher.serverCache.get(cacheKey) };
}
} else {
const cachedData = this.clientCache.get(cacheKey);
if (cachedData && Date.now() - cachedData.timestamp < this.cacheExpirationTime) {
console.log(`Using cached data for ${this.options.componentId} from client cache`);
return { data: cachedData.data };
}
}
const url = this.getUrl(isServer);
try {
const response = await fetch(url, {
cache: "no-store"
});
if (!response.ok) {
throw new Error(`Failed to fetch CSV data: ${response.statusText}`);
}
const text = await response.text();
const rows = text.split("\n");
const headers = rows[0].split(",");
const jsonData = rows.slice(1).filter((row) => row.trim() !== "").map((row) => {
const values = row.split(",");
return headers.reduce((obj, header, index) => {
var _a;
obj[header.trim()] = (_a = values[index]) == null ? void 0 : _a.trim();
return obj;
}, {});
});
const parsedData = this.parseData(jsonData);
if (isServer) {
_BaseFetcher.serverCache.set(cacheKey, parsedData);
} else {
this.clientCache.set(cacheKey, {
data: parsedData,
timestamp: Date.now()
});
}
return { data: parsedData };
} catch (error) {
console.error("Error fetching CSV data:", error);
throw error;
}
}
async fetchTxtData(isServer) {
const cacheKey = `txt_${this.options.componentId}`;
if (isServer) {
if (_BaseFetcher.serverCache.has(cacheKey)) {
console.log(`Using cached data for ${this.options.componentId} from server cache`);
return { data: _BaseFetcher.serverCache.get(cacheKey) };
}
} else {
const cachedData = this.clientCache.get(cacheKey);
if (cachedData && Date.now() - cachedData.timestamp < this.cacheExpirationTime) {
console.log(`Using cached data for ${this.options.componentId} from client cache`);
return { data: cachedData.data };
}
}
const url = this.getUrl(isServer);
try {
const response = await fetch(url, {
cache: "no-store"
});
if (!response.ok) {
throw new Error(`Failed to fetch TXT data: ${response.statusText}`);
}
const text = await response.text();
const lines = text.split("\n");
const jsonData = lines.filter((line) => line.trim() !== "").map((line) => {
try {
return JSON.parse(line);
} catch (e) {
const pairs = line.split(",");
return pairs.reduce((obj, pair) => {
const [key, value] = pair.split(":").map((s) => s.trim());
if (key && value) {
obj[key] = value;
}
return obj;
}, {});
}
});
const parsedData = this.parseData(jsonData);
if (isServer) {
_BaseFetcher.serverCache.set(cacheKey, parsedData);
} else {
this.clientCache.set(cacheKey, {
data: parsedData,
timestamp: Date.now()
});
}
return { data: parsedData };
} catch (error) {
console.error("Error fetching TXT data:", error);
throw error;
}
}
// Update the fetchApiData method to include retry logic and better error handling
async fetchApiData(isServer) {
const cacheKey = `api_${this.options.componentId}`;
if (isServer) {
if (_BaseFetcher.serverCache.has(cacheKey)) {
console.log(`Using cached data for ${this.options.componentId} from server cache`);
return { data: _BaseFetcher.serverCache.get(cacheKey) };
}
} else {
const cachedData = this.clientCache.get(cacheKey);
if (cachedData && Date.now() - cachedData.timestamp < this.cacheExpirationTime) {
console.log(`Using cached data for ${this.options.componentId} from client cache`);
return { data: cachedData.data };
}
}
const url = this.options.endpoint || "";
if (!url || url === "") {
console.warn(`No endpoint provided for API data source in component ${this.options.componentId}`);
return { data: [] };
}
const MAX_RETRIES = 3;
let retries = 0;
let lastError = null;
while (retries < MAX_RETRIES) {
try {
let headers = {};
if (process.env.NEXT_PUBLIC_RAPIDAPI_KEY && process.env.NEXT_PUBLIC_RAPIDAPI_HOST) {
headers = {
"x-rapidapi-key": process.env.NEXT_PUBLIC_RAPIDAPI_KEY,
"x-rapidapi-host": process.env.NEXT_PUBLIC_RAPIDAPI_HOST
};
}
console.log(`Fetching API data from ${url}, attempt ${retries + 1}`);
const response = await fetch(url, {
headers,
cache: "no-store"
});
if (response.status === 429) {
const retryAfter = response.headers.get("Retry-After") || "5";
const waitTime = Number.parseInt(retryAfter, 10) * 1e3 || 2 ** retries * 1e3;
console.warn(`Rate limited. Retrying after ${waitTime}ms...`);
await new Promise((resolve) => setTimeout(resolve, waitTime));
retries++;
continue;
}
if (!response.ok) {
throw new Error(`Failed to fetch API data: ${response.statusText}`);
}
const data = await response.json();
const parsedData = this.parseData(data);
if (isServer) {
_BaseFetcher.serverCache.set(cacheKey, parsedData);
} else {
this.clientCache.set(cacheKey, {
data: parsedData,
timestamp: Date.now()
});
}
return { data: parsedData };
} catch (error) {
lastError = error;
console.error(`Error fetching API data from ${url} (attempt ${retries + 1}):`, error);
const waitTime = 2 ** retries * 1e3;
await new Promise((resolve) => setTimeout(resolve, waitTime));
retries++;
}
}
console.error(`Failed to fetch API data after ${MAX_RETRIES} attempts. Using fallback data.`);
try {
if (!isServer) {
const componentId = this.options.componentId;
const fallbackUrl = `/api/fallback?component=${componentId}`;
console.log(`Trying fallback data from: ${fallbackUrl}`);
const response = await fetch(fallbackUrl);
if (response.ok) {
const data = await response.json();
const parsedData = this.parseData(data);
return { data: parsedData };
}
}
} catch (fallbackError) {
console.error("Error fetching fallback data:", fallbackError);
}
return { data: this.getMockData() };
}
// Add a new method to provide mock data as a last resort
getMockData() {
const componentId = this.options.componentId;
if (componentId.includes("User")) {
return [
{ id: 1, name: "Mock User 1", email: "user1@example.com" },
{ id: 2, name: "Mock User 2", email: "user2@example.com" },
{ id: 3, name: "Mock User 3", email: "user3@example.com" }
];
} else if (componentId.includes("Product")) {
return [
{ id: 1, name: "Mock Product 1", price: 99.99, description: "A mock product" },
{ id: 2, name: "Mock Product 2", price: 199.99, description: "Another mock product" },
{ id: 3, name: "Mock Product 3", price: 299.99, description: "Yet another mock product" }
];
}
return [];
}
// Update pagination settings
setPagination(page, limit, enabled = true) {
if (this.options.pagination) {
this.options.pagination.page = page;
this.options.pagination.limit = limit;
this.options.pagination.enabled = enabled;
} else {
this.options.pagination = { page, limit, enabled };
}
}
// Publish a data change event
publishDataChange(action, data, id) {
realtimeManager.publish({
componentId: this.options.componentId,
action,
data,
id
});
}
async fetchData(isServer = false) {
console.log(`Fetching data for ${this.options.componentId} with isServer=${isServer}`);
const dataSource = this.options.dataSource || "json";
switch (dataSource) {
case "json":
return this.fetchJsonData(isServer);
case "csv":
return this.fetchCsvData(isServer);
case "txt":
return this.fetchTxtData(isServer);
case "api":
return this.fetchApiData(isServer);
default:
throw new Error(`Unsupported data source: ${dataSource}`);
}
}
};
var BaseFetcher = _BaseFetcher;
// Add a static cache for server-side data
BaseFetcher.serverCache = /* @__PURE__ */ new Map();
// src/hooks/useRealtimeUpdates.ts
import { useEffect, useRef, useState } from "react";
function useRealtimeUpdates(componentId, onUpdate) {
const eventSourceRef = useRef(null);
const reconnectTimeoutRef = useRef(null);
const [connectionAttempts, setConnectionAttempts] = useState(0);
const MAX_RECONNECT_ATTEMPTS = 5;
useEffect(() => {
const unsubscribe = realtimeManager.subscribe(componentId, () => {
onUpdate();
});
const setupEventSource = () => {
if (connectionAttempts >= MAX_RECONNECT_ATTEMPTS) {
console.warn(`Exceeded maximum reconnection attempts (${MAX_RECONNECT_ATTEMPTS}). Giving up.`);
return;
}
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
try {
const timestamp = (/* @__PURE__ */ new Date()).getTime();
const eventSource = new EventSource(`/api/sse?t=${timestamp}`);
eventSourceRef.current = eventSource;
eventSource.onopen = () => {
console.log("SSE connection established");
setConnectionAttempts(0);
};
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === "connected") {
console.log("Connected to SSE stream with client ID:", data.clientId);
return;
}
if (data.componentId === componentId) {
console.log("Received SSE update for component:", componentId);
onUpdate();
}
} catch (error) {
console.error("Error parsing SSE message:", error);
}
};
eventSource.onerror = (event) => {
console.warn("SSE connection error. Attempting to reconnect...");
eventSource.close();
eventSourceRef.current = null;
setConnectionAttempts((prev) => prev + 1);
const backoffTime = Math.min(1e3 * Math.pow(2, connectionAttempts), 3e4);
console.log(`Reconnecting in ${backoffTime / 1e3} seconds...`);
reconnectTimeoutRef.current = setTimeout(() => {
setupEventSource();
}, backoffTime);
};
} catch (error) {
console.error("Error setting up SSE connection:", error);
}
};
if (typeof window !== "undefined") {
setupEventSource();
}
return () => {
unsubscribe();
if (eventSourceRef.current) {
eventSourceRef.current.close();
eventSourceRef.current = null;
}
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
};
}, [componentId, onUpdate, connectionAttempts]);
}
// src/hocs/withClientFetching.tsx
import { useEffect as useEffect2, useState as useState2, useCallback, useRef as useRef2 } from "react";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
function withClientFetching(WrappedComponent, componentId, options = {}) {
const displayName = WrappedComponent.displayName || WrappedComponent.name || "Component";
const {
dataSource = "json",
enableRealtime = false,
defaultItemsPerPage = 10,
loadingComponent = /* @__PURE__ */ jsx("div", { children: "Loading data..." }),
errorComponent = (error, retry) => /* @__PURE__ */ jsxs("div", { className: "error-container p-4 border border-red-300 rounded-md bg-red-50", children: [
/* @__PURE__ */ jsxs("div", { className: "text-red-500 mb-2", children: [
"Error: ",
error
] }),
/* @__PURE__ */ jsx("button", { onClick: retry, className: "px-3 py-1 bg-blue-500 text-white rounded-md hover:bg-blue-600 text-sm", children: "Retry" })
] })
} = options;
function ClientComponent(props) {
const [data, setData] = useState2([]);
const [loading, setLoading] = useState2(true);
const [error, setError] = useState2(null);
const [retryCount, setRetryCount] = useState2(0);
const [currentPage, setCurrentPage] = useState2(1);
const [itemsPerPage, setItemsPerPage] = useState2(defaultItemsPerPage);
const [totalItems, setTotalItems] = useState2(void 0);
const [totalPages, setTotalPages] = useState2(void 0);
const fetcherRef = useRef2(null);
const cacheKey = `client_${componentId}_${dataSource}_page${currentPage}_limit${itemsPerPage}`;
useRealtimeUpdates(componentId, () => {
if (fetcherRef.current) {
fetcherRef.current.invalidateCache();
}
setRetryCount((prev) => prev + 1);
});
const handleRetry = useCallback(() => {
setLoading(true);
setError(null);
setRetryCount((prev) => prev + 1);
}, []);
const handlePageChange = useCallback((page) => {
setCurrentPage(page);
}, []);
const handleItemsPerPageChange = useCallback((items) => {
setItemsPerPage(items);
setCurrentPage(1);
}, []);
useEffect2(() => {
let isMounted = true;
const fetchData = async () => {
if (!isMounted)
return;
setLoading(true);
setError(null);
try {
const registry = FetcherRegistry.getInstance();
const fetcher = registry.getFetcher(componentId);
if (!fetcher) {
throw new Error(`No fetcher registered for component: ${componentId}`);
}
fetcherRef.current = fetcher;
fetcher.setPagination(currentPage, itemsPerPage, true);
console.log(`Client-side fetching for: ${componentId} (page ${currentPage}, limit ${itemsPerPage})`);
const result = await fetcher.fetchData(false);
if (isMounted) {
if (result.data.length === 0 && currentPage > 1) {
setCurrentPage(1);
} else {
setData(result.data);
if (result.totalItems !== void 0) {
setTotalItems(result.totalItems);
}
if (result.totalPages !== void 0) {
setTotalPages(result.totalPages);
} else if (result.totalItems !== void 0) {
setTotalPages(Math.ceil(result.totalItems / itemsPerPage));
}
if (result.data.length === 0) {
setError("No data available. The API might be rate limited.");
}
}
}
} catch (err) {
if (isMounted) {
console.error("Client fetching error:", err);
let errorMessage = err.message || "Unknown error occurred";
if (errorMessage.includes("Too Many Requests") || errorMessage.includes("429")) {
errorMessage = "The API is rate limited. Please try again later.";
} else if (errorMessage.includes("Failed to fetch")) {
errorMessage = "Network error. Please check your connection.";
}
setError(errorMessage);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
fetchData();
return () => {
isMounted = false;
};
}, [componentId, currentPage, itemsPerPage]);
if (loading) {
return /* @__PURE__ */ jsx(Fragment, { children: loadingComponent });
}
if (error) {
return /* @__PURE__ */ jsx(Fragment, { children: typeof errorComponent === "function" ? errorComponent(error, handleRetry) : errorComponent });
}
const enhancedProps = {
...props,
data,
pagination: {
currentPage,
totalPages: totalPages || Math.ceil(data.length / itemsPerPage),
totalItems: totalItems || data.length,
itemsPerPage,
onPageChange: handlePageChange,
onItemsPerPageChange: handleItemsPerPageChange
}
};
return /* @__PURE__ */ jsx(WrappedComponent, { ...enhancedProps });
}
ClientComponent.displayName = `withClientFetching(${displayName})`;
return ClientComponent;
}
// src/hocs/withServerFetching.tsx
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
function withServerFetching(WrappedComponent, componentId, options = {}) {
const displayName = WrappedComponent.displayName || WrappedComponent.name || "Component";
const {
defaultItemsPerPage = 10,
loadingComponent = /* @__PURE__ */ jsx2("div", { children: "Loading data..." }),
errorComponent = (error) => /* @__PURE__ */ jsxs2("div", { children: [
"Error: ",
error
] })
} = options;
async function ServerComponent(props) {
console.log(`Server-side fetching for: ${componentId}`);
try {
const registry = FetcherRegistry.getInstance();
const fetcher = registry.getFetcher(componentId);
if (!fetcher) {
throw new Error(`No fetcher registered for component: ${componentId}`);
}
fetcher.setPagination(1, defaultItemsPerPage, true);
const result = await fetcher.fetchData(true);
const enhancedProps = {
...props,
data: result.data,
pagination: {
currentPage: 1,
totalPages: result.totalPages || Math.ceil(result.data.length / defaultItemsPerPage),
totalItems: result.totalItems || result.data.length,
itemsPerPage: defaultItemsPerPage
}
};
return /* @__PURE__ */ jsx2(WrappedComponent, { ...enhancedProps });
} catch (error) {
console.error("Server fetching error:", error);
return /* @__PURE__ */ jsx2(Fragment2, { children: typeof errorComponent === "function" ? errorComponent(error.message || "Unknown error occurred") : errorComponent });
}
}
ServerComponent.displayName = `withServerFetching(${displayName})`;
return ServerComponent;
}
// src/components/ListRenderers.tsx
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
function ListRenderer({
data,
renderItem,
title,
className = "list-container",
listClassName = "list",
itemClassName = "list-item"
}) {
return /* @__PURE__ */ jsxs3("div", { className, children: [
/* @__PURE__ */ jsx3("h2", { children: title }),
data.length === 0 ? /* @__PURE__ */ jsx3("p", { children: "No data available" }) : /* @__PURE__ */ jsx3("ul", { className: listClassName, children: data.map((item, index) => /* @__PURE__ */ jsx3("li", { className: itemClassName, children: renderItem(item, index) }, index)) })
] });
}
// src/components/DynamicListRenderer.tsx
import { useState as useState4, useEffect as useEffect3, useRef as useRef3 } from "react";
// src/components/Pagination.tsx
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
function Pagination({
currentPage,
totalPages,
onPageChange,
itemsPerPage,
onItemsPerPageChange,
totalItems,
showItemsPerPage = true,
className = ""
}) {
const getPageNumbers = () => {
const pageNumbers = [];
const maxPagesToShow = 5;
if (totalPages <= maxPagesToShow) {
for (let i = 1; i <= totalPages; i++) {
pageNumbers.push(i);
}
} else {
pageNumbers.push(1);
let start = Math.max(2, currentPage - 1);
let end = Math.min(totalPages - 1, currentPage + 1);
if (currentPage <= 3) {
end = Math.min(totalPages - 1, 4);
}
if (currentPage >= totalPages - 2) {
start = Math.max(2, totalPages - 3);
}
if (start > 2) {
pageNumbers.push("...");
}
for (let i = start; i <= end; i++) {
pageNumbers.push(i);
}
if (end < totalPages - 1) {
pageNumbers.push("...");
}
pageNumbers.push(totalPages);
}
return pageNumbers;
};
return /* @__PURE__ */ jsxs4(
"div",
{
className: `pagination-container flex flex-col sm:flex-row items-center justify-between gap-4 mt-4 w-full ${className}`,
children: [
/* @__PURE__ */ jsx4("div", { className: "flex items-center text-sm text-gray-500", children: totalItems !== void 0 && /* @__PURE__ */ jsxs4("span", { children: [
"Showing ",
Math.min((currentPage - 1) * itemsPerPage + 1, totalItems),
" to",
" ",
Math.min(currentPage * itemsPerPage, totalItems),
" of ",
totalItems,
" items"
] }) }),
/* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-1", children: [
/* @__PURE__ */ jsx4(
"button",
{
onClick: () => onPageChange(1),
disabled: currentPage === 1,
className: "p-2 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:pointer-events-none",
"aria-label": "First page",
children: "<<"
}
),
/* @__PURE__ */ jsx4(
"button",
{
onClick: () => onPageChange(currentPage - 1),
disabled: currentPage === 1,
className: "p-2 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:pointer-events-none",
"aria-label": "Previous page",
children: "<"
}
),
getPageNumbers().map(
(page, index) => typeof page === "number" ? /* @__PURE__ */ jsx4(
"button",
{
onClick: () => onPageChange(page),
className: `px-3 py-2 rounded-md ${currentPage === page ? "bg-blue-500 text-white" : "hover:bg-gray-100"}`,
children: page
},
index
) : /* @__PURE__ */ jsx4("span", { className: "px-3 py-2", children: page }, index)
),
/* @__PURE__ */ jsx4(
"button",
{
onClick: () => onPageChange(currentPage + 1),
disabled: currentPage === totalPages,
className: "p-2 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:pointer-events-none",
"aria-label": "Next page",
children: ">"
}
),
/* @__PURE__ */ jsx4(
"button",
{
onClick: () => onPageChange(totalPages),
disabled: currentPage === totalPages,
className: "p-2 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:pointer-events-none",
"aria-label": "Last page",
children: ">>"
}
)
] }),
showItemsPerPage && onItemsPerPageChange && /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", children: [
/* @__PURE__ */ jsx4("span", { className: "text-sm text-gray-500", children: "Items per page:" }),
/* @__PURE__ */ jsxs4(
"select",
{
value: itemsPerPage,
onChange: (e) => onItemsPerPageChange(Number(e.target.value)),
className: "p-1 border rounded-md text-sm bg-white",
children: [
/* @__PURE__ */ jsx4("option", { value: "5", children: "5" }),
/* @__PURE__ */ jsx4("option", { value: "10", children: "10" }),
/* @__PURE__ */ jsx4("option", { value: "20", children: "20" }),
/* @__PURE__ */ jsx4("option", { value: "50", children: "50" }),
/* @__PURE__ */ jsx4("option", { value: "100", children: "100" })
]
}
)
] })
]
}
);
}
// src/components/DynamicDataDisplay.tsx
import { useState as useState3 } from "react";
import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
function DynamicDataDisplay({
data,
excludeFields = [],
priorityFields = [],
className = ""
}) {
const [expanded, setExpanded] = useState3(false);
if (!data || typeof data !== "object") {
return /* @__PURE__ */ jsx5("div", { children: "No data available" });
}
const allFields = Object.keys(data).filter((key) => !excludeFields.includes(key));
const priorityFieldsToShow = allFields.filter((field) => priorityFields.includes(field));
const otherFields = allFields.filter((field) => !priorityFields.includes(field));
const formatValue = (value) => {
if (value === null || value === void 0)
return "N/A";
if (typeof value === "object") {
if (Array.isArray(value)) {
return value.length > 0 ? `[Array(${value.length})]` : "[]";
}
return "[Object]";
}
return String(value);
};
return /* @__PURE__ */ jsxs5("div", { className: `dynamic-data-display ${className}`, children: [
priorityFieldsToShow.map((field) => /* @__PURE__ */ jsxs5("div", { className: "field-row mb-1", children: [
/* @__PURE__ */ jsxs5("span", { className: "field-name font-medium", children: [
field,
":"
] }),
" ",
/* @__PURE__ */ jsx5("span", { className: "field-value", children: formatValue(data[field]) })
] }, field)),
otherFields.slice(0, expanded ? otherFields.length : 2).map((field) => /* @__PURE__ */ jsxs5("div", { className: "field-row mb-1", children: [
/* @__PURE__ */ jsxs5("span", { className: "field-name font-medium", children: [
field,
":"
] }),
" ",
/* @__PURE__ */ jsx5("span", { className: "field-value", children: formatValue(data[field]) })
] }, field)),
otherFields.length > 2 && /* @__PURE__ */ jsx5(
"button",
{
onClick: () => setExpanded(!expanded),
className: "text-sm flex items-center mt-1 text-blue-500 hover:underline",
children: expanded ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
/* @__PURE__ */ jsx5("span", { className: "mr-1", children: "\u25BC" }),
" Show less"
] }) : /* @__PURE__ */ jsxs5(Fragment3, { children: [
/* @__PURE__ */ jsx5("span", { className: "mr-1", children: "\u25B6" }),
" Show ",
otherFields.length - 2,
" more fields"
] })
}
)
] });
}
// src/components/DynamicListRenderer.tsx
import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
function DynamicListRenderer({
data,
title,
priorityFields = [],
excludeFields = [],
itemsPerPage: defaultItemsPerPage = 10,
virtualized = false,
className = "",
listClassName = "",
itemClassName = ""
}) {
const [currentPage, setCurrentPage] = useState4(1);
const [itemsPerPage, setItemsPerPage] = useState4(defaultItemsPerPage);
const [visibleItems, setVisibleItems] = useState4([]);
const containerRef = useRef3(null);
const totalPages = Math.max(1, Math.ceil(data.length / itemsPerPage));
const handlePageChange = (page) => {
setCurrentPage(page);
if (containerRef.current) {
containerRef.current.scrollTop = 0;
}
};
const handleItemsPerPageChange = (newItemsPerPage) => {
setItemsPerPage(newItemsPerPage);
setCurrentPage(1);
};
useEffect3(() => {
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
setVisibleItems(data.slice(startIndex, endIndex));
}, [currentPage, itemsPerPage, data]);
const renderVirtualizedList = () => {
return /* @__PURE__ */ jsx6("div", { ref: containerRef, className: `virtualized-list-container overflow-auto max-h-[500px] ${listClassName}`, children: visibleItems.map((item, index) => /* @__PURE__ */ jsx6(
"div",
{
className: `list-item p-4 border rounded-md mb-2 hover:shadow-md transition-shadow ${itemClassName}`,
children: /* @__PURE__ */ jsx6(DynamicDataDisplay, { data: item, priorityFields, excludeFields })
},
index
)) });
};
const renderStandardList = () => {
return /* @__PURE__ */ jsx6("div", { className: `list-container ${listClassName}`, children: /* @__PURE__ */ jsx6("ul", { className: "list", children: visibleItems.map((item, index) => /* @__PURE__ */ jsx6(
"li",
{
className: `list-item p-4 border rounded-md mb-2 hover:shadow-md transition-shadow ${itemClassName}`,
children: /* @__PURE__ */ jsx6(DynamicDataDisplay, { data: item, priorityFields, excludeFields })
},
index
)) }) });
};
return /* @__PURE__ */ jsxs6("div", { className: `dynamic-list-renderer ${className}`, children: [
/* @__PURE__ */ jsx6("h2", { className: "text-xl font-bold mb-4", children: title }),
data.length === 0 ? /* @__PURE__ */ jsx6("p", { className: "text-gray-500", children: "No data available" }) : /* @__PURE__ */ jsxs6(Fragment4, { children: [
virtualized ? renderVirtualizedList() : renderStandardList(),
/* @__PURE__ */ jsx6(
Pagination,
{
currentPage,
totalPages,
onPageChange: handlePageChange,
itemsPerPage,
onItemsPerPageChange: handleItemsPerPageChange,
totalItems: data.length
}
)
] })
] });
}
// src/components/Toggle.tsx
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
function Toggle({
onToggleMode,
onChangeDataSource,
onRefresh,
isServer,
dataSource,
isRealtime = false,
onToggleRealtime,
className = ""
}) {
return /* @__PURE__ */ jsxs7(
"div",
{
className: `toggle-container flex flex-col md:flex-row justify-between gap-4 p-4 bg-gray-100 rounded-lg ${className}`,
children: [
/* @__PURE__ */ jsxs7("div", { className: "mode-toggle", children: [
/* @__PURE__ */ jsx7("h3", { className: "font-medium mb-2", children: "Fetch Mode:" }),
/* @__PURE__ */ jsxs7("div", { className: "toggle-buttons flex gap-2", children: [
/* @__PURE__ */ jsx7(
"button",
{
className: `px-3 py-1 rounded-md ${isServer ? "bg-blue-500 text-white" : "bg-gray-200"}`,
onClick: () => onToggleMode(true),
children: "Server-side"
}
),
/* @__PURE__ */ jsx7(
"button",
{
className: `px-3 py-1 rounded-md ${!isServer ? "bg-blue-500 text-white" : "bg-gray-200"}`,
onClick: () => onToggleMode(false),
children: "Client-side"
}
)
] })
] }),
/* @__PURE__ */ jsxs7("div", { className: "data-source-toggle", children: [
/* @__PURE__ */ jsx7("h3", { className: "font-medium mb-2", children: "Data Source:" }),
/* @__PURE__ */ jsxs7(
"select",
{
value: dataSource,
onChange: (e) => onChangeDataSource(e.target.value),
className: "px-3 py-1 rounded-md bg-white border",
children: [
/* @__PURE__ */ jsx7("option", { value: "json", children: "JSON" }),
/* @__PURE__ */ jsx7("option", { value: "csv", children: "CSV" }),
/* @__PURE__ */ jsx7("option", { value: "txt", children: "TXT" }),
/* @__PURE__ */ jsx7("option", { value: "api", children: "API" })
]
}
)
] }),
onToggleRealtime && /* @__PURE__ */ jsxs7("div", { className: "realtime-toggle", children: [
/* @__PURE__ */ jsx7("h3", { className: "font-medium mb-2", children: "Real-time Updates:" }),
/* @__PURE__ */ jsxs7("div", { className: "toggle-buttons flex gap-2", children: [
/* @__PURE__ */ jsx7(
"button",
{
className: `px-3 py-1 rounded-md ${isRealtime ? "bg-blue-500 text-white" : "bg-gray-200"}`,
onClick: onToggleRealtime,
children: isRealtime ? "Enabled" : "Disabled"
}
),
onRefresh && /* @__PURE__ */ jsxs7(
"button",
{
onClick: onRefresh,
className: "px-3 py-1 rounded-md bg-gray-200 flex items-center gap-1",
"aria-label": "Refresh data",
children: [
/* @__PURE__ */ jsx7("span", { className: "refresh-icon", children: "\u21BB" }),
"Refresh"
]
}
)
] })
] })
]
}
);
}
// src/utils/index.ts
function createDataApiHandler(dataDir = "app/data") {
return async (req) => {
const url = new URL(req.url);
const component = url.searchParams.get("component");
const dataSource = url.searchParams.get("dataSource") || "json";
const page = Number.parseInt(url.searchParams.get("page") || "1", 10);
const limit = Number.parseInt(url.searchParams.get("limit") || "0", 10);
if (!component) {
return new Response(JSON.stringify({ error: "Component parameter is required" }), {
status: 400,
headers: { "Content-Type": "application/json" }
});
}
try {
const mockData = [
{ id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" },
{ id: 3, name: "Item 3" }
];
let paginatedData = mockData;
const totalItems = mockData.length;
if (limit > 0) {
const startIndex = (page - 1) * limit;
paginatedData = mockData.slice(startIndex, startIndex + limit);
}
if (dataSource === "json") {
return new Response(
JSON.stringify({
data: paginatedData,
pagination: limit > 0 ? {
page,
limit,
totalItems,
totalPages: Math.ceil(totalItems / limit)
} : null
}),
{
headers: { "Content-Type": "application/json" }
}
);
} else {
return new Response("Mock data for non-JSON format", {
headers: { "Content-Type": dataSource === "csv" ? "text/csv" : "text/plain" }
});
}
} catch (error) {
return new Response(JSON.stringify({ error: error.message || "Failed to fetch data" }), {
status: 500,
headers: { "Content-Type": "application/json" }
});
}
};
}
function createSseHandler() {
return async (req) => {
const clientId = Math.random().toString(36).substring(2, 15);
const stream = new ReadableStream({
start(controller) {
const initialData = `data: ${JSON.stringify({ type: "connected", clientId })}
`;
controller.enqueue(new TextEncoder().encode(initialData));
const keepAliveInterval = setInterval(() => {
try {
controller.enqueue(new TextEncoder().encode(`: keep-alive
`));
} catch (error) {
clearInterval(keepAliveInterval);
}
}, 3e4);
req.signal.addEventListener("abort", () => {
clearInterval(keepAliveInterval);
console.log(`Client ${clientId} disconnected`);
});
}
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-store, no-transform",
Connection: "keep-alive",
"X-Accel-Buffering": "no"
// Disable buffering for Nginx
}
});
};
}
export {
BaseFetcher,
DynamicDataDisplay,
DynamicListRenderer,
FetcherRegistry,
ListRenderer,
Pagination,
Toggle,
createDataApiHandler,
createSseHandler,
realtimeManager,
useRealtimeUpdates,
withClientFetching,
withServerFetching
};
//# sourceMappingURL=index.mjs.map