From 47bd2d8a2cc05c731bbef92f17957a1d6270749f Mon Sep 17 00:00:00 2001 From: Meshmulla Date: Mon, 23 Feb 2026 15:01:56 +0100 Subject: [PATCH] Create Subscribers management table --- ...RDING.md\n" => TODO_CREATOR_ONBOARDING.md_ | 0 .../src/creators/dto/onboard-creator.dto.ts_ | 0 .../src/creators/entities/creator.entity.ts_ | 0 frontend/package-lock.json | 42 +-- .../src/app/dashboard/subscribers/page.tsx | 7 +- .../components/dashboard/SubscribersTable.tsx | 312 ++++++++++++++++++ 6 files changed, 323 insertions(+), 38 deletions(-) rename "TODO_CREATOR_ONBOARDING.md\n" => TODO_CREATOR_ONBOARDING.md_ (100%) rename "backend/src/creators/dto/onboard-creator.dto.ts\n" => backend/src/creators/dto/onboard-creator.dto.ts_ (100%) rename "backend/src/creators/entities/creator.entity.ts\n" => backend/src/creators/entities/creator.entity.ts_ (100%) create mode 100644 frontend/src/components/dashboard/SubscribersTable.tsx diff --git "a/TODO_CREATOR_ONBOARDING.md\n" b/TODO_CREATOR_ONBOARDING.md_ similarity index 100% rename from "TODO_CREATOR_ONBOARDING.md\n" rename to TODO_CREATOR_ONBOARDING.md_ diff --git "a/backend/src/creators/dto/onboard-creator.dto.ts\n" b/backend/src/creators/dto/onboard-creator.dto.ts_ similarity index 100% rename from "backend/src/creators/dto/onboard-creator.dto.ts\n" rename to backend/src/creators/dto/onboard-creator.dto.ts_ diff --git "a/backend/src/creators/entities/creator.entity.ts\n" b/backend/src/creators/entities/creator.entity.ts_ similarity index 100% rename from "backend/src/creators/entities/creator.entity.ts\n" rename to backend/src/creators/entities/creator.entity.ts_ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9dbb7d4..813922d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -67,7 +67,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1587,7 +1586,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1648,7 +1646,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -2105,7 +2102,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2431,7 +2427,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3083,7 +3078,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3261,7 +3255,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5349,7 +5342,6 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5358,7 +5350,6 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -5376,7 +5367,6 @@ "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -5424,8 +5414,7 @@ "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "peer": true + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -6077,7 +6066,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -6229,7 +6217,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6519,7 +6506,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -6566,7 +6552,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, - "peer": true, "requires": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -7484,7 +7469,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, - "peer": true, "requires": { "csstype": "^3.2.2" } @@ -7530,7 +7514,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, - "peer": true, "requires": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -7781,8 +7764,7 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "peer": true + "dev": true }, "acorn-jsx": { "version": "5.3.2", @@ -7998,7 +7980,6 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, - "peer": true, "requires": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -8475,7 +8456,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, - "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8600,7 +8580,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, - "peer": true, "requires": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -9953,14 +9932,12 @@ "react": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", - "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", - "peer": true + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==" }, "react-dom": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", - "peer": true, "requires": { "scheduler": "^0.27.0" } @@ -9975,7 +9952,6 @@ "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", - "peer": true, "requires": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -10002,8 +9978,7 @@ "redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "peer": true + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, "redux-thunk": { "version": "3.1.0", @@ -10446,8 +10421,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "peer": true + "dev": true } } }, @@ -10561,8 +10535,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "peer": true + "dev": true }, "typescript-eslint": { "version": "8.56.0", @@ -10760,8 +10733,7 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, - "peer": true + "dev": true }, "zod-validation-error": { "version": "4.0.2", diff --git a/frontend/src/app/dashboard/subscribers/page.tsx b/frontend/src/app/dashboard/subscribers/page.tsx index caf77b6..d46501b 100644 --- a/frontend/src/app/dashboard/subscribers/page.tsx +++ b/frontend/src/app/dashboard/subscribers/page.tsx @@ -1,10 +1,11 @@ +import React from 'react'; +import SubscribersTable from '@/components/dashboard/SubscribersTable'; + export default function SubscribersPage() { return (

Subscribers

-
-

View and manage your subscriber base.

-
+
); } diff --git a/frontend/src/components/dashboard/SubscribersTable.tsx b/frontend/src/components/dashboard/SubscribersTable.tsx new file mode 100644 index 0000000..895e9eb --- /dev/null +++ b/frontend/src/components/dashboard/SubscribersTable.tsx @@ -0,0 +1,312 @@ +'use client'; + +import React, { useState, useMemo } from 'react'; +import { Search, Download, ArrowUpDown, ArrowUp, ArrowDown, ChevronLeft, ChevronRight } from 'lucide-react'; +import Badge from '../ui/Badge'; + +// Mock Data +type SubscriberStatus = 'Active' | 'Cancelled' | 'Past Due'; + +interface Subscriber { + id: string; + name: string; + email: string; + avatar: string; + plan: string; + tier: string; + joinDate: string; + renewDate: string; + status: SubscriberStatus; + totalPaid: number; +} + +const MOCK_DATA: Subscriber[] = [ + { id: '1', name: 'Alice Smith', email: 'alice@example.com', avatar: 'https://i.pravatar.cc/150?u=1', plan: 'VIP Access', tier: '$20/mo', joinDate: '2023-01-15', renewDate: '2023-02-15', status: 'Active', totalPaid: 240 }, + { id: '2', name: 'Bob Johnson', email: 'bob@example.com', avatar: 'https://i.pravatar.cc/150?u=2', plan: 'Supporter', tier: '$5/mo', joinDate: '2023-05-20', renewDate: '2023-06-20', status: 'Active', totalPaid: 45 }, + { id: '3', name: 'Charlie Brown', email: 'charlie@example.com', avatar: 'https://i.pravatar.cc/150?u=3', plan: 'VIP Access', tier: '$20/mo', joinDate: '2022-11-01', renewDate: '2023-11-01', status: 'Past Due', totalPaid: 400 }, + { id: '4', name: 'Diana Prince', email: 'diana@example.com', avatar: 'https://i.pravatar.cc/150?u=4', plan: 'Exclusive Content', tier: '$10/mo', joinDate: '2023-06-10', renewDate: '2023-07-10', status: 'Cancelled', totalPaid: 30 }, + { id: '5', name: 'Evan Davis', email: 'evan@example.com', avatar: 'https://i.pravatar.cc/150?u=5', plan: 'Supporter', tier: '$5/mo', joinDate: '2023-07-01', renewDate: '2023-08-01', status: 'Active', totalPaid: 5 }, + { id: '6', name: 'Fiona Gallagher', email: 'fiona@example.com', avatar: 'https://i.pravatar.cc/150?u=6', plan: 'VIP Access', tier: '$20/mo', joinDate: '2023-02-28', renewDate: '2023-03-28', status: 'Active', totalPaid: 100 }, + { id: '7', name: 'George Miller', email: 'george@example.com', avatar: 'https://i.pravatar.cc/150?u=7', plan: 'Exclusive Content', tier: '$10/mo', joinDate: '2023-03-15', renewDate: '2023-04-15', status: 'Active', totalPaid: 50 }, + { id: '8', name: 'Hannah Abbott', email: 'hannah@example.com', avatar: 'https://i.pravatar.cc/150?u=8', plan: 'Supporter', tier: '$5/mo', joinDate: '2023-04-10', renewDate: '2023-05-10', status: 'Cancelled', totalPaid: 15 }, + { id: '9', name: 'Ian Malcolm', email: 'ian@example.com', avatar: 'https://i.pravatar.cc/150?u=9', plan: 'VIP Access', tier: '$20/mo', joinDate: '2023-01-05', renewDate: '2023-02-05', status: 'Past Due', totalPaid: 20 }, + { id: '10', name: 'Julia Roberts', email: 'julia@example.com', avatar: 'https://i.pravatar.cc/150?u=10', plan: 'Exclusive Content', tier: '$10/mo', joinDate: '2023-05-05', renewDate: '2023-06-05', status: 'Active', totalPaid: 20 }, + { id: '11', name: 'Kevin Hart', email: 'kevin@example.com', avatar: 'https://i.pravatar.cc/150?u=11', plan: 'Supporter', tier: '$5/mo', joinDate: '2023-06-25', renewDate: '2023-07-25', status: 'Active', totalPaid: 10 }, + { id: '12', name: 'Laura Dern', email: 'laura@example.com', avatar: 'https://i.pravatar.cc/150?u=12', plan: 'VIP Access', tier: '$20/mo', joinDate: '2023-02-14', renewDate: '2023-03-14', status: 'Active', totalPaid: 120 }, +]; + +type SortConfig = { + key: keyof Subscriber | null; + direction: 'asc' | 'desc'; +}; + +export default function SubscribersTable() { + const [search, setSearch] = useState(''); + const [statusFilter, setStatusFilter] = useState('All'); + const [sortConfig, setSortConfig] = useState({ key: 'joinDate', direction: 'desc' }); + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 5; + + // Filter, Sort, Paginate + const processedData = useMemo(() => { + let result = MOCK_DATA; + + // Search + if (search) { + const lowerSearch = search.toLowerCase(); + result = result.filter(sub => sub.name.toLowerCase().includes(lowerSearch) || sub.email.toLowerCase().includes(lowerSearch)); + } + + // Filter + if (statusFilter !== 'All') { + result = result.filter(sub => sub.status === statusFilter); + } + + // Sort + if (sortConfig.key) { + result = [...result].sort((a, b) => { + if (a[sortConfig.key!] < b[sortConfig.key!]) { + return sortConfig.direction === 'asc' ? -1 : 1; + } + if (a[sortConfig.key!] > b[sortConfig.key!]) { + return sortConfig.direction === 'asc' ? 1 : -1; + } + return 0; + }); + } + + return result; + }, [search, statusFilter, sortConfig]); + + // Pagination + const totalPages = Math.ceil(processedData.length / itemsPerPage); + const paginatedData = processedData.slice( + (currentPage - 1) * itemsPerPage, + currentPage * itemsPerPage + ); + + // Status Badge Helper + const getStatusBadge = (status: SubscriberStatus) => { + switch (status) { + case 'Active': return Active; + case 'Cancelled': return Cancelled; + case 'Past Due': return Past Due; + default: return {status}; + } + }; + + const handleSort = (key: keyof Subscriber) => { + let direction: 'asc' | 'desc' = 'asc'; + if (sortConfig.key === key && sortConfig.direction === 'asc') { + direction = 'desc'; + } + setSortConfig({ key, direction }); + }; + + const SortIcon = ({ columnKey }: { columnKey: keyof Subscriber }) => { + if (sortConfig.key !== columnKey) return ; + return sortConfig.direction === 'asc' ? : ; + }; + + // Export CSV + const handleExportCSV = () => { + const headers = ['Name', 'Email', 'Plan', 'Tier', 'Join Date', 'Renew Date', 'Status', 'Total Paid']; + const csvContent = [ + headers.join(','), + ...processedData.map(sub => `"${sub.name}","${sub.email}","${sub.plan}","${sub.tier}","${sub.joinDate}","${sub.renewDate}","${sub.status}","${sub.totalPaid}"`) + ].join('\n'); + + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + if (link.download !== undefined) { + const url = URL.createObjectURL(blob); + link.setAttribute('href', url); + link.setAttribute('download', 'subscribers_export.csv'); + link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + }; + + // Reset page when filters change + React.useEffect(() => { + setCurrentPage(1); + }, [search, statusFilter]); + + return ( +
+ {/* Controls */} +
+
+
+
+ +
+ setSearch(e.target.value)} + className="w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-sm text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-colors" + /> +
+
+ +
+
+ + +
+ + {/* Table / Cards */} +
+ {/* Desktop Table */} +
+ + + + + + + + + + + + {paginatedData.length > 0 ? paginatedData.map((sub, idx) => ( + + + + + + + + )) : ( + + + + )} + +
handleSort('name')}> +
Fan
+
handleSort('plan')}> +
Plan
+
handleSort('joinDate')}> +
Dates
+
handleSort('status')}> +
Status
+
handleSort('totalPaid')}> +
Total Paid
+
+
+ {sub.name} +
+
{sub.name}
+
{sub.email}
+
+
+
+
{sub.plan}
+
{sub.tier}
+
+
Joined: {sub.joinDate}
+
Renews: {sub.renewDate}
+
+ {getStatusBadge(sub.status)} + + ${sub.totalPaid.toFixed(2)} +
+ No subscribers found matching your criteria. +
+
+ + {/* Mobile Cards */} +
+ {paginatedData.length > 0 ? paginatedData.map((sub) => ( +
+
+
+ {sub.name} +
+
{sub.name}
+
{sub.email}
+
+
+
+ {getStatusBadge(sub.status)} +
+
+ +
+
+
Plan
+
{sub.plan} ({sub.tier})
+
+
+
Total Paid
+
${sub.totalPaid.toFixed(2)}
+
+
+
Joined
+
{sub.joinDate}
+
+
+
Renews
+
{sub.renewDate}
+
+
+
+ )) : ( +
+ No subscribers found matching your criteria. +
+ )} +
+ + {/* Pagination Controls */} + {totalPages > 0 && ( +
+ + Showing {(currentPage - 1) * itemsPerPage + 1} to {Math.min(currentPage * itemsPerPage, processedData.length)} of {processedData.length} results + +
+ + +
+
+ )} +
+
+ ); +}