UNPKG

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
// 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