diff --git a/package-lock.json b/package-lock.json index d02693d..3e3e01f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@reduxjs/toolkit": "^2.11.2", "axios": "^1.13.6", "lucide-react": "^0.577.0", + "prop-types": "^15.8.1", "react": "^19.2.0", "react-dom": "^19.2.0", "react-hook-form": "^7.71.2", @@ -3708,7 +3709,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -4120,6 +4120,18 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -4270,6 +4282,15 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -4494,6 +4515,23 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/property-expr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", diff --git a/package.json b/package.json index dfe64cf..747e9f4 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@reduxjs/toolkit": "^2.11.2", "axios": "^1.13.6", "lucide-react": "^0.577.0", + "prop-types": "^15.8.1", "react": "^19.2.0", "react-dom": "^19.2.0", "react-hook-form": "^7.71.2", diff --git a/src/components/common/EmptyState.jsx b/src/components/common/EmptyState.jsx new file mode 100644 index 0000000..78ad29a --- /dev/null +++ b/src/components/common/EmptyState.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button } from './button'; + +const EmptyState = ({ icon, title, description, actionLabel, onAction }) => { + return ( +
+ {icon &&
{icon}
} +

{title}

+

{description}

+ {actionLabel && onAction && ( + + )} +
+ ); +}; + +EmptyState.propTypes = { + icon: PropTypes.node, + title: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + actionLabel: PropTypes.string, + onAction: PropTypes.func, +}; + +export default EmptyState; \ No newline at end of file diff --git a/src/components/dashboard/RecentCampaigns.jsx b/src/components/dashboard/RecentCampaigns.jsx index 01a4f8c..9f6dcab 100644 --- a/src/components/dashboard/RecentCampaigns.jsx +++ b/src/components/dashboard/RecentCampaigns.jsx @@ -4,9 +4,11 @@ import { ArrowRight, PlusCircle, LayoutGrid } from 'lucide-react'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; import { fetchRecentCampaigns } from '../../features/dashboard/dashboardThunks'; import { + import { selectRecentCampaigns, selectDashboardLoading, } from '../../features/dashboard/dashboardSelectors'; +import EmptyState from '../common/EmptyState'; // ─── Status badge config ────────────────────────────────────────────────────── const STATUS_BADGE = { @@ -105,22 +107,13 @@ export default function RecentCampaigns() { ))} ) : recent.length === 0 ? ( - // Empty state -
- -

No campaigns yet

-

- You have not created any campaigns. Launch your first one and start - collecting donations. -

- - - Create Your First Campaign - -
+ } + title="No campaigns yet" + description="You have not created any campaigns. Launch your first one and start collecting donations." + actionLabel="Create Your First Campaign" + onAction={() => (window.location.href = '/campaigns/create')} + /> ) : ( // Campaign cards grid
diff --git a/src/pages/dashboard/MyBookmarksPage.jsx b/src/pages/dashboard/MyBookmarksPage.jsx new file mode 100644 index 0000000..4560193 --- /dev/null +++ b/src/pages/dashboard/MyBookmarksPage.jsx @@ -0,0 +1,21 @@ +import { BookmarkX } from 'lucide-react'; +import EmptyState from '../../components/common/EmptyState'; + +export default function MyBookmarksPage() { + return ( +
+

My Bookmarks

+

Your bookmarked campaigns will appear here.

+ +
+ } + title="No bookmarks yet" + description="You haven't bookmarked any campaigns yet. Browse campaigns and bookmark your favorites." + actionLabel="Browse Campaigns" + onAction={() => (window.location.href = '/campaigns')} + /> +
+
+ ); +} \ No newline at end of file diff --git a/src/routes/AppRouter.jsx b/src/routes/AppRouter.jsx index d26d6e3..53968a6 100644 --- a/src/routes/AppRouter.jsx +++ b/src/routes/AppRouter.jsx @@ -26,6 +26,7 @@ const Admin = lazy(() => import('../pages/Admin')); // Dashboard sub-pages const DashboardPage = lazy(() => import('../pages/dashboard/DashboardPage')); const MyCampaignsPage = lazy(() => import('../pages/dashboard/MyCampaignsPage')); +const MyBookmarksPage = lazy(() => import('../pages/dashboard/MyBookmarksPage')); const DonationsPage = lazy(() => import('../pages/dashboard/DonationsPage')); const WalletPage = lazy(() => import('../pages/dashboard/WalletPage')); const SettingsPage = lazy(() => import('../pages/dashboard/SettingsPage')); @@ -70,6 +71,7 @@ const AppRouter = () => { }> } /> } /> + } /> } /> } /> } />