├── .gitignore ├── LICENSE ├── README.md ├── custom.d.ts ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── Logo.svg ├── Logo3.svg ├── campaign.svg ├── footerLogo.svg ├── hero-image.svg ├── image.svg ├── logo2.svg ├── mobile-bg.svg ├── mobile-bg2.svg └── vite.svg ├── src ├── App.tsx ├── Routes.tsx ├── UI │ ├── Modal │ │ └── CustomModal.tsx │ └── TabComponent │ │ └── tabs.tsx ├── assets │ ├── Call.svg │ ├── Category.svg │ ├── Delete.svg │ ├── Envelope.svg │ ├── Location.svg │ ├── Profile.svg │ ├── Time Circle.svg │ ├── academic.svg │ ├── arrow icon.svg │ ├── campaign icon (2).svg │ ├── campaign icon.svg │ ├── charity (2).svg │ ├── community (2).svg │ ├── disaster.svg │ ├── error.svg │ ├── frame.svg │ ├── fund icon.svg │ ├── icon (2).png │ ├── icons │ │ ├── Category.svg │ │ ├── Chart.svg │ │ ├── Setting.svg │ │ └── Wallet.svg │ ├── image (3).svg │ ├── image (4).svg │ ├── image (5).svg │ ├── image (6).svg │ ├── legal.svg │ ├── medical.svg │ ├── message icon.svg │ ├── people icon.svg │ ├── react.svg │ ├── student 1.svg │ ├── student 2.svg │ ├── student 3.svg │ ├── student4.svg │ ├── tabler_currency-naira.svg │ └── union.svg ├── components │ ├── Button.tsx │ ├── CampaignInput.tsx │ ├── Date.tsx │ ├── Footer.tsx │ ├── HandleFirebaseAction.tsx │ ├── Information │ │ ├── BasicInformation.tsx │ │ ├── CampaignInformation.tsx │ │ └── ContactInformation.tsx │ ├── Inputs.tsx │ ├── Loader.tsx │ ├── Nav.tsx │ ├── Sidebar.tsx │ ├── Topbar.tsx │ ├── Transaction │ │ └── table.tsx │ ├── campaign │ │ ├── CampaignCard.tsx │ │ ├── CampaignDetails.tsx │ │ ├── CampaignLoader.tsx │ │ ├── CreateCampaigns.tsx │ │ └── alcampaign.tsx │ ├── donate │ │ └── donate.tsx │ ├── settings │ │ ├── EditProfile.tsx │ │ ├── Feedback.tsx │ │ └── Security.tsx │ └── spolightCards.tsx ├── context │ ├── createCampaign.context.tsx │ ├── settings.context.tsx │ ├── sidebar.context.tsx │ └── viewCampaign.context.tsx ├── firebase.ts ├── helpers │ ├── axiosConfig.ts │ └── config.ts ├── hook │ └── redux.hook.ts ├── icons │ ├── DownloadIcon.tsx │ ├── Facebookicon.tsx │ ├── InstagramIcon.tsx │ ├── LinkedinIcon.tsx │ ├── TwitterIcon.tsx │ └── YoutubeIcon.tsx ├── index.css ├── main.tsx ├── redux │ ├── slices │ │ └── user.slice.ts │ └── store.ts ├── types │ ├── auth │ │ ├── createAccount.ts │ │ ├── forgotPassword.ts │ │ ├── login.ts │ │ └── reset-password.ts │ ├── campaign.ts │ ├── components │ │ ├── button.ts │ │ ├── input.ts │ │ └── spotlightCampaign.ts │ ├── createVA.ts │ ├── donate.ts │ ├── images.ts │ ├── settings.ts │ └── user.slice.ts ├── utils │ ├── errorMapping.ts │ ├── numberFormat.tsx │ ├── requests │ │ ├── campaign.request.ts │ │ ├── donate.request.ts │ │ ├── transactions.request.ts │ │ └── user.request.ts │ └── userInitials.ts ├── views │ ├── Dashboard │ │ ├── Campaigns.tsx │ │ ├── Overview.tsx │ │ ├── Profile.tsx │ │ ├── Settings.tsx │ │ ├── Transactions.tsx │ │ └── layout.tsx │ ├── ErrorPage.tsx │ ├── LandingPage │ │ ├── HeroSection.tsx │ │ ├── How.tsx │ │ ├── Mobilize.tsx │ │ ├── Spotlight.tsx │ │ ├── contact.tsx │ │ └── index.tsx │ └── auth │ │ ├── AuthSidebar.tsx │ │ ├── CreateAccount.tsx │ │ ├── EmailVerification.tsx │ │ ├── ForgotPassword.tsx │ │ ├── Layout.tsx │ │ ├── Login.tsx │ │ ├── ProtectedRoute.tsx │ │ ├── ResetPassword.tsx │ │ ├── ResetPasswordConfirm.tsx │ │ ├── VerificationSuccessful.tsx │ │ └── providerWrapper.tsx └── vite-env.d.ts ├── supporthive ├── .eslintrc ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── sanity.cli.ts ├── sanity.config.ts ├── sanity.query.ts ├── schemaTypes │ ├── campaign.ts │ ├── index.ts │ └── user.ts ├── static │ └── .gitkeep └── tsconfig.json ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vercel.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 SupportHive 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About the Project 2 | 3 | SupportHive is a community-based crowdfunding platform designed to empower individuals and organizations to create campaigns, Whether it's a school fundraiser, a community development project, supporting less privileged children, unemployed graduates, or providing emergency assistance, SupportHive enables users to easily raise funds, and track progress in a transparent and accessible way. 4 | 5 | **Demo:** 6 | 7 | Screenshot 2024-10-24 at 12 06 47 PM 8 | 9 | ## **Watch demo video:** 10 | 11 | https://www.awesomescreenshot.com/video/32914565?key=c0acf6916b6c7abb2e599ccab88a5a33 12 | 13 | ## **Visit this website** 14 | https://support-hive.vercel.app/ 15 | 16 | ## **Check Figma Design** 17 | https://www.figma.com/design/cFBRfD9pGk81EbVgud8Fdn/SupportHive?node-id=0-1&t=v43DdWriBvalCcOy-1 18 | 19 | **Key Features:** 20 | - Create Campaigns: Individuals and organizations can create campaigns for various causes. 21 | - Secure Donations: Integrated with Paystack for secure and reliable payment processing. 22 | - Track Progress: Campaign creators and supporters can track the progress of each campaign. 23 | - Community-Focused: Designed to build trust and collaboration within communities. 24 | 25 | **Tech Stack**: 26 | 27 | - Frontend: React, Tailwind CSS 28 | - Database: Sanity 29 | - Backend: Firebase (Authentication, API) 30 | - Payments: Paystack for secure online payments 31 | 32 | # Getting Started 33 | 34 | Here’s how to set up the project locally. 35 | 36 | Prerequisites: 37 | 38 | - [Node.js](https://nodejs.org) installed 39 | - [Git](https://git-scm.com) installed 40 | - [npm](https://www.npmjs.com) (Node Package Manager) installed 41 | - A code editor such as [Visual Studio Code](https://code.visualstudio.com) or any other preferred code environment 42 | 43 | Ensure you have these prerequisites installed and set up on your system before proceeding with the project setup. 44 | 45 | **Installation** 46 | 47 | 1. **Fork the Repository**: Fork the [Supporthive](https://github.com/preshpi/SupportHive) project to your own GitHub account. 48 | 49 | 2. **Clone the Repository**: Clone the forked repository to your local machine by running the following command in your terminal: 50 | 51 | ```bash 52 | git clone https://github.com/your-username/SupportHive 53 | ``` 54 | 55 | 3. **Create a New Branch**: Create a new branch to work on your desired features using the following command: 56 | 57 | ```bash 58 | git checkout -b feature 59 | ``` 60 | 61 | 4. **Install Dependencies**: Navigate to the project folder on your terminal and install the dependencies using the following command: 62 | 63 | ```bash 64 | npm install 65 | ``` 66 | 67 | 5. **Run the Front-end**: Start the application by running the following command: 68 | 69 | ```bash 70 | npm run dev 71 | ``` 72 | 73 | 6. **Make Changes**: Now that you have set up your environment and created a new branch, you can start making your changes. You can modify existing code, add new features, or fix bugs. 74 | 75 | 7. **Commit Your Changes**: Once you have made your changes, commit them to your local branch using the following commands: 76 | 77 | ```bash 78 | git add . 79 | git commit -m "Your commit message" 80 | ``` 81 | 82 | 8. **Push Changes**: Push your changes to your main branch and upload them to your GitHub account using the following command: 83 | 84 | ```bash 85 | git push origin "name of your branch" 86 | ``` 87 | 88 | 9. **Open a Pull Request**: Finally, open a pull request in the original [SupportHive repository](https://github.com/preshpi/SupportHive/pulls) 89 | 90 | # Contributors 91 | 92 | ## Developers 93 | 94 | 95 | 96 | 97 | ## Product designers 98 | 99 | - Egwuenu Faith 100 | - Azeezat Funmilayo Taiwo 101 | - Ruth Ezeneche 102 | 103 | 104 | # License 105 | SupportHive is distributed under the MIT [License](https://github.com/preshpi/SupportHive/blob/main/LICENSE). See the LICENSE file for more information. 106 | -------------------------------------------------------------------------------- /custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | import * as React from "react"; 3 | 4 | export const ReactComponent: React.FunctionComponent< 5 | React.SVGProps & { title?: string } 6 | >; 7 | 8 | const src: string; 9 | export default src; 10 | } 11 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | SupportHive 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supporthive", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@emailjs/browser": "^4.4.1", 14 | "@hookform/resolvers": "^3.9.0", 15 | "@paystack/inline-js": "^2.22.0", 16 | "@reduxjs/toolkit": "^2.3.0", 17 | "@sanity/asset-utils": "^2.0.6", 18 | "@sanity/client": "^6.22.2", 19 | "@sanity/image-url": "^1.0.2", 20 | "@sanity/vision": "^3.61.0", 21 | "@types/jest": "^29.5.13", 22 | "@types/node": "^22.7.5", 23 | "@types/react": "^18.3.11", 24 | "@types/react-dom": "^18.3.0", 25 | "aos": "^3.0.0-beta.6", 26 | "axios": "^1.7.7", 27 | "date-fns": "^4.1.0", 28 | "dom": "^0.0.3", 29 | "firebase": "^10.14.1", 30 | "js-cookie": "^3.0.5", 31 | "react": "^18.3.1", 32 | "react-dom": "^18.3.1", 33 | "react-hook-form": "^7.53.0", 34 | "react-icons": "^5.3.0", 35 | "react-number-format": "^5.4.2", 36 | "react-paystack": "^6.0.0", 37 | "react-redux": "^9.1.2", 38 | "react-router-dom": "^6.26.2", 39 | "react-share": "^5.1.0", 40 | "redux-persist": "^6.0.0", 41 | "router": "^1.3.8", 42 | "sanity": "^3.61.0", 43 | "sonner": "^1.5.0", 44 | "typescript": "^5.6.3", 45 | "uuid": "^10.0.0", 46 | "zod": "^3.23.8" 47 | }, 48 | "devDependencies": { 49 | "@eslint/js": "^9.11.1", 50 | "@types/aos": "^3.0.7", 51 | "@types/js-cookie": "^3.0.6", 52 | "@types/paystack__inline-js": "^1.0.0", 53 | "@types/react": "^18.3.11", 54 | "@types/react-dom": "^18.3.0", 55 | "@types/redux-persist": "^4.0.0", 56 | "@vitejs/plugin-react": "^4.3.2", 57 | "autoprefixer": "^10.4.20", 58 | "eslint": "^9.11.1", 59 | "eslint-plugin-react": "^7.37.0", 60 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 61 | "eslint-plugin-react-refresh": "^0.4.12", 62 | "globals": "^15.9.0", 63 | "postcss": "^8.4.47", 64 | "tailwindcss": "^3.4.13", 65 | "vite": "^5.4.8" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/Logo3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/logo2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import AppRoutes from "./Routes"; 2 | import AOS from "aos"; 3 | import "aos/dist/aos.css"; 4 | import { useEffect } from "react"; 5 | 6 | const App = () => { 7 | useEffect(() => { 8 | AOS.init({ 9 | delay: 0, // values from 0 to 3000, with step 50ms 10 | duration: 400, // values from 0 to 3000, with step 50ms 11 | easing: "ease", // default easing for AOS animations 12 | once: false, 13 | }); 14 | AOS.refresh(); 15 | }, []); 16 | 17 | return ( 18 |
19 | 20 |
21 | ); 22 | }; 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /src/Routes.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; 2 | import ResetPassword from "./views/auth/ResetPassword.jsx"; 3 | import ResetPasswordConfirm from "./views/auth/ResetPasswordConfirm.js"; 4 | import EmailVerification from "./views/auth/EmailVerification.js"; 5 | import CreateAccount from "./views/auth/CreateAccount.js"; 6 | import ForgotPassword from "./views/auth/ForgotPassword.js"; 7 | import VerificationSuccessful from "./views/auth/VerificationSuccessful.js"; 8 | import Login from "./views/auth/Login.js"; 9 | import LandingPage from "./views/LandingPage/index.js"; 10 | import Overview from "./views/Dashboard/Overview.js"; 11 | import Dashboardlayout from "./views/Dashboard/layout.js"; 12 | import Campaigns from "./views/Dashboard/Campaigns.js"; 13 | import Transactions from "./views/Dashboard/Transactions.js"; 14 | import Settings from "./views/Dashboard/Settings.js"; 15 | import ProtectedRoute from "./views/auth/ProtectedRoute.js"; 16 | import HandleFirebaseAction from "./components/HandleFirebaseAction.js"; 17 | import CampaignDetails from "./components/campaign/CampaignDetails.js"; 18 | import CreateCampaigns from "./components/campaign/CreateCampaigns.js"; 19 | import Donate from "./components/donate/donate.js"; 20 | import Profile from "./views/Dashboard/Profile.js"; 21 | import ErrorPage from "./views/ErrorPage.js"; 22 | 23 | const AppRoutes = () => { 24 | return ( 25 | 26 | 27 | {/* dashboard */} 28 | }> 29 | } /> 30 | } /> 31 | 35 | 36 | 37 | } 38 | /> 39 | } /> 40 | } 44 | /> 45 | 49 | 50 | 51 | } 52 | /> 53 | 54 | 58 | 59 | 60 | } 61 | /> 62 | 66 | 67 | 68 | } 69 | /> 70 | 74 | 75 | 76 | } 77 | /> 78 | 79 | 80 | 81 | {/* Auth Routes */} 82 | } /> 83 | } 86 | /> 87 | } /> 88 | } /> 89 | } /> 90 | } /> 91 | } 94 | /> 95 | } /> 96 | 97 | {/* landing page */} 98 | } /> 99 | 100 | 101 | ); 102 | }; 103 | 104 | export default AppRoutes; 105 | -------------------------------------------------------------------------------- /src/UI/Modal/CustomModal.tsx: -------------------------------------------------------------------------------- 1 | type ConfirmationModalProps = { 2 | isOpen: boolean; 3 | onClose: () => void; 4 | onConfirm: () => void; 5 | }; 6 | 7 | const ConfirmationModal = ({ 8 | isOpen, 9 | onClose, 10 | onConfirm, 11 | }: ConfirmationModalProps) => { 12 | if (!isOpen) return null; 13 | 14 | return ( 15 |
16 |
17 |

Confirm Submission

18 |

Are you sure you want to submit this campaign?

19 |
20 | 26 | 32 |
33 |
34 |
35 | ); 36 | }; 37 | 38 | export default ConfirmationModal; 39 | -------------------------------------------------------------------------------- /src/UI/TabComponent/tabs.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface TabProps { 4 | tabs: string[]; 5 | activeTab: number; 6 | onTabChange: (index: number) => void; 7 | } 8 | 9 | const Tabs: React.FC = ({ tabs, activeTab, onTabChange }) => { 10 | return ( 11 |
12 | {tabs.map((tab, index) => ( 13 | 20 | ))} 21 |
22 | ); 23 | }; 24 | 25 | export default Tabs; 26 | -------------------------------------------------------------------------------- /src/assets/Call.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/Category.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/Delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/Envelope.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/Location.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/Profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/Time Circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/arrow icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/campaign icon (2).svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/campaign icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/fund icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icon (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preshpi/SupportHive/114e2554ef5b159e86e8562246963af14062750b/src/assets/icon (2).png -------------------------------------------------------------------------------- /src/assets/icons/Category.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/Chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/Setting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/Wallet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/message icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/people icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/tabler_currency-naira.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonProps } from "../types/components/button"; 2 | import Loader from "./Loader"; 3 | 4 | export function Button({ 5 | children, 6 | className, 7 | onClick, 8 | disabled, 9 | loading, 10 | ...props 11 | }: ButtonProps) { 12 | return ( 13 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/components/CampaignInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface CampaignInputProps { 4 | label: string; 5 | placeholder: string; 6 | value: string; 7 | onChange: (e: React.ChangeEvent) => void; 8 | className?: string; 9 | } 10 | 11 | const CampaignInput: React.FC = ({ 12 | label, 13 | placeholder, 14 | value, 15 | onChange, 16 | className, 17 | }) => { 18 | return ( 19 |
20 | 21 | 28 |
29 | ); 30 | }; 31 | 32 | export default CampaignInput; 33 | -------------------------------------------------------------------------------- /src/components/Date.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | const DateFilter: React.FC<{ onFilterChange: (value: string) => void }> = ({ onFilterChange }) => { 4 | const [selected, setSelected] = useState('Last 7 Days'); 5 | 6 | const handleChange = (e: React.ChangeEvent) => { 7 | setSelected(e.target.value); 8 | onFilterChange(e.target.value); 9 | }; 10 | 11 | return ( 12 |
13 | 23 |
24 | ); 25 | }; 26 | 27 | export default DateFilter; 28 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | // import FacebookIcon from "../icons/Facebookicon"; 2 | // import InstagramIcon from "../icons/InstagramIcon"; 3 | // import LinkedinIcon from "../icons/LinkedinIcon"; 4 | // import Twittericon from "../icons/TwitterIcon"; 5 | // import YoutubeIcon from "../icons/YoutubeIcon"; 6 | 7 | const Footer = () => { 8 | return ( 9 |
10 |
11 |
12 | 13 |
14 |

15 | @ 2024 supporthive.All Rights Reserved. 16 |

17 | {/*
18 |
19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |

27 | @ 2024 supporthive.All Rights Reserved. 28 |

29 |
30 |
*/} 31 |
32 | ); 33 | }; 34 | 35 | export default Footer; 36 | -------------------------------------------------------------------------------- /src/components/HandleFirebaseAction.tsx: -------------------------------------------------------------------------------- 1 | import { useSearchParams } from "react-router-dom"; 2 | import { applyActionCode, verifyPasswordResetCode } from "firebase/auth"; // Firebase imports 3 | import { auth } from "../firebase"; 4 | import { toast } from "sonner"; 5 | 6 | const HandleFirebaseAction = () => { 7 | const [searchParams] = useSearchParams(); 8 | const mode = searchParams.get("mode"); 9 | const oobCode = searchParams.get("oobCode"); 10 | 11 | // Handling different actions based on the 'mode' 12 | switch (mode) { 13 | case "resetPassword": 14 | // Redirect to reset password page 15 | handlePasswordReset(oobCode); 16 | break; 17 | 18 | case "verifyEmail": 19 | // Verify the email 20 | handleEmailVerification(oobCode); 21 | break; 22 | 23 | // You can add more cases for other actions like recoverEmail, etc. 24 | 25 | default: 26 | toast.error("Unknown mode"); 27 | } 28 | 29 | return
Handling Firebase Action...
; 30 | }; 31 | 32 | // Function to handle password reset 33 | const handlePasswordReset = async (oobCode: string | null) => { 34 | try { 35 | await verifyPasswordResetCode(auth, oobCode as string); 36 | window.location.href = `/reset-password?oobCode=${oobCode}`; 37 | } catch (error) { 38 | toast.error((error as { message: string }).message); 39 | } 40 | }; 41 | 42 | // Function to handle email verification 43 | const handleEmailVerification = async (oobCode: string | null) => { 44 | try { 45 | await applyActionCode(auth, oobCode as string); 46 | window.location.href = "/email-verified-success"; 47 | } catch (error) { 48 | toast.error((error as { message: string }).message); 49 | } 50 | }; 51 | 52 | export default HandleFirebaseAction; 53 | -------------------------------------------------------------------------------- /src/components/Information/BasicInformation.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Arrow from "../../assets/arrow icon.svg"; 3 | import { useFormContext } from "react-hook-form"; 4 | import Input from "../Inputs"; 5 | 6 | interface BasicInformationProps { 7 | onNext: () => void; 8 | } 9 | 10 | const BasicInformation: React.FC = ({ onNext }) => { 11 | const { 12 | register, 13 | formState: { errors }, 14 | trigger, 15 | } = useFormContext(); 16 | const handleNext = async () => { 17 | const isValid = await trigger(["title", "country", "city", "category"]); 18 | if (isValid) onNext(); 19 | }; 20 | 21 | return ( 22 |
23 |
24 | 32 | {errors.title && ( 33 | {`${errors.title.message}`} 34 | )} 35 |
36 |
37 | 45 | {errors.country && ( 46 | {`${errors.country.message}`} 47 | )} 48 |
49 |
50 | 58 | {errors.city && ( 59 | {`${errors.city.message}`} 60 | )} 61 |
62 |
63 | 78 | {errors.category && ( 79 | 80 | {errors.category?.message as string} 81 | 82 | )} 83 |
84 |
85 | 92 |
93 |
94 | ); 95 | }; 96 | 97 | export default BasicInformation; 98 | -------------------------------------------------------------------------------- /src/components/Information/CampaignInformation.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Arrow from "../../assets/arrow icon.svg"; 3 | import { useFormContext } from "react-hook-form"; 4 | import Input from "../Inputs"; 5 | import { toast } from "sonner"; 6 | 7 | interface CampaignInformationProps { 8 | onNext: () => void; 9 | } 10 | 11 | const CampaignInformation: React.FC = ({ 12 | onNext, 13 | }) => { 14 | const { 15 | register, 16 | formState: { errors }, 17 | trigger, 18 | } = useFormContext(); 19 | const handleNext = async () => { 20 | const isValid = await trigger([ 21 | "description", 22 | "goalAmount", 23 | "startDate", 24 | "endDate", 25 | "raiseMoneyFor", 26 | "importance", 27 | "impact", 28 | "images", 29 | // "supportingDocuments", 30 | ]); 31 | if (isValid) { 32 | onNext(); 33 | } else { 34 | toast.error(`Validation Errors: ${JSON.stringify(errors)}`); // Log errors to see the issues 35 | } 36 | }; 37 | 38 | interface CustomFormData { 39 | name: string; 40 | images: File[]; 41 | } 42 | 43 | const [formData, setFormData] = useState({ 44 | name: "", 45 | images: [], 46 | }); 47 | 48 | const handleImagesChange = (e: React.ChangeEvent) => { 49 | const files = Array.from(e.target.files || []); // Convert FileList to an array 50 | 51 | setFormData({ ...formData, images: files }); 52 | }; 53 | 54 | return ( 55 |
56 |
57 | 65 | {errors.description && ( 66 | {`${errors.description.message}`} 67 | )} 68 |
69 |
70 | 78 | {errors.goalAmount && ( 79 | {`${errors.goalAmount.message}`} 80 | )} 81 |
82 | 83 |
84 |
85 | 93 | {errors.startDate && ( 94 | {`${errors.startDate.message}`} 95 | )} 96 |
97 |
98 | 106 | {errors.endDate && ( 107 | {`${errors.endDate.message}`} 108 | )} 109 |
110 |
111 |
112 | 118 | 123 | 124 | {errors.raiseMoneyFor && ( 125 | {`${errors.raiseMoneyFor.message}`} 126 | )} 127 |
128 |
129 | 135 | 140 | {errors.importance && ( 141 | {`${errors.importance.message}`} 142 | )} 143 |
144 |
145 | 151 | 156 | 157 | {errors.impact && ( 158 | {`${errors.impact.message}`} 159 | )} 160 |
161 |
162 | 165 | 174 |
175 | 176 | {/*
177 | 180 | 181 | 190 |
*/} 191 | 192 |
193 | 200 |
201 |
202 | ); 203 | }; 204 | 205 | export default CampaignInformation; 206 | -------------------------------------------------------------------------------- /src/components/Inputs.tsx: -------------------------------------------------------------------------------- 1 | import { useState, forwardRef } from "react"; 2 | import { LuEye, LuEyeOff } from "react-icons/lu"; 3 | import { InputProps } from "../types/components/input"; 4 | 5 | const Input = forwardRef( 6 | ( 7 | { 8 | label, 9 | value, 10 | name, 11 | id, 12 | type, 13 | additionalClasses, 14 | placeholder, 15 | onChange, 16 | additionalAttributes, 17 | textarea, 18 | password, 19 | pattern, 20 | rows, 21 | readOnly, 22 | cols, 23 | disabled, 24 | required, 25 | minLength, 26 | maxLength, 27 | autoComplete, 28 | options, 29 | multiple, 30 | accept, 31 | optionsKey, 32 | }, 33 | ref 34 | ) => { 35 | const [visible, setVisible] = useState(false); 36 | 37 | return ( 38 |
39 | 42 | {!textarea && !options && !optionsKey && ( 43 |
44 | 68 | {password && ( 69 | 70 | 80 | 81 | )} 82 |
83 | )} 84 | {textarea && ( 85 | 95 | )} 96 | {options && ( 97 | 126 | )} 127 | 128 | {optionsKey && ( 129 | 158 | )} 159 |
160 | ); 161 | } 162 | ); 163 | 164 | Input.displayName = "Input"; 165 | 166 | export default Input; 167 | -------------------------------------------------------------------------------- /src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | const Loader = () => { 2 | return
; 3 | }; 4 | 5 | export default Loader; 6 | -------------------------------------------------------------------------------- /src/components/Nav.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | import { FaBars, FaTimes } from "react-icons/fa"; 4 | import { useState } from "react"; 5 | 6 | const NavBar = () => { 7 | const [isOpen, setIsOpen] = useState(false); 8 | 9 | const toggleMenu = () => { 10 | setIsOpen(!isOpen); 11 | }; 12 | 13 | return ( 14 |
15 |
16 |
17 |
18 | Logo 19 |
20 | 21 |
22 | 29 |
30 | 31 |
32 | How it works 33 | Browse Campaigns 34 | 35 | 38 | 39 |
40 |
41 | 42 |
45 |
46 | How it works 47 | Browse Campaigns 48 | 49 | 52 | 53 |
54 |
55 |
56 |
57 | ); 58 | }; 59 | 60 | export default NavBar; 61 | -------------------------------------------------------------------------------- /src/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { Link, NavLink, useNavigate } from "react-router-dom"; 2 | import { IoClose } from "react-icons/io5"; 3 | import { useAppContext } from "../context/sidebar.context"; 4 | import { CiLogout, CiUser } from "react-icons/ci"; 5 | import { signOut } from "firebase/auth"; 6 | import { logoutUser } from "../redux/slices/user.slice"; 7 | import { useAppDispatch } from "../hook/redux.hook"; 8 | import { auth } from "../firebase"; 9 | import { toast } from "sonner"; 10 | import { TbArrowsTransferUp, TbLayoutDashboardFilled } from "react-icons/tb"; 11 | import { IconType } from "react-icons/lib"; 12 | import { MdCampaign } from "react-icons/md"; 13 | import { IoMdSettings } from "react-icons/io"; 14 | import React from "react"; 15 | 16 | type TLinks = { 17 | name: string; 18 | path: string; 19 | icon: IconType; 20 | }; 21 | const Sidebar = () => { 22 | const { isSideBarOpen, setIsSideBarOpen } = useAppContext(); 23 | const dispatch = useAppDispatch(); 24 | const navigate = useNavigate(); 25 | 26 | const links: TLinks[] = [ 27 | { 28 | name: "Dashboard", 29 | path: "/dashboard/overview", 30 | icon: TbLayoutDashboardFilled, 31 | }, 32 | { 33 | name: "Campaigns", 34 | path: "/dashboard/campaigns", 35 | icon: MdCampaign, 36 | }, 37 | { 38 | name: "Transactions", 39 | path: "/dashboard/transactions", 40 | icon: TbArrowsTransferUp, 41 | }, 42 | { 43 | name: "Profile", 44 | path: "/dashboard/profile", 45 | icon: CiUser, 46 | }, 47 | { 48 | name: "Settings", 49 | path: "/dashboard/settings", 50 | icon: IoMdSettings, 51 | }, 52 | ]; 53 | 54 | const handleLogout = async () => { 55 | try { 56 | // Sign out from Firebase 57 | await signOut(auth); 58 | 59 | // Clear localStorage or cookie if necessary 60 | localStorage.removeItem("authToken"); 61 | 62 | // Dispatch logout action to reset the Redux state 63 | dispatch(logoutUser()); 64 | 65 | // Redirect user to home page 66 | navigate("/"); 67 | toast.success("Successfully signed out."); 68 | } catch (error) { 69 | toast.error((error as { message: string }).message); 70 | } 71 | }; 72 | 73 | return ( 74 | <> 75 | {isSideBarOpen && ( 76 | 120 | )} 121 | 122 | ); 123 | }; 124 | 125 | export default Sidebar; 126 | -------------------------------------------------------------------------------- /src/components/Topbar.tsx: -------------------------------------------------------------------------------- 1 | import { useAppContext } from "../context/sidebar.context"; 2 | import { FiMenu } from "react-icons/fi"; 3 | import { RootState } from "../redux/store"; 4 | import { useSelector } from "react-redux"; 5 | import { getInitials } from "../utils/userInitials"; 6 | import { Link, useNavigate } from "react-router-dom"; 7 | 8 | const Topbar = () => { 9 | const { isSideBarOpen, setIsSideBarOpen } = useAppContext(); 10 | const userDetails = useSelector((state: RootState) => state.user); // Get user details from Redux store 11 | 12 | const { firstname, lastname } = userDetails.userDetails; 13 | const initials = getInitials(firstname, lastname); // Get initials 14 | 15 | const navigate = useNavigate(); 16 | const handleGOBack = () => { 17 | navigate(-1); 18 | }; 19 | return ( 20 |
21 |
22 |
23 | {!isSideBarOpen && ( 24 | 30 | )} 31 | 35 |
36 | 37 | 38 |
39 | {/*
40 | 41 |
*/} 42 |
43 | {/*

44 | {firstname} {lastname} 45 |

*/} 46 |
47 |

{initials}

48 |
49 |
50 |
51 | 52 |
53 |
54 | ); 55 | }; 56 | 57 | export default Topbar; 58 | -------------------------------------------------------------------------------- /src/components/Transaction/table.tsx: -------------------------------------------------------------------------------- 1 | import NumberFormat from "../../utils/numberFormat"; 2 | 3 | interface Transaction { 4 | id: number; 5 | status: string; 6 | amount: number; 7 | customer: { 8 | email: string; 9 | }; 10 | subaccount: { 11 | business_name: string; 12 | }; 13 | created_at: string; 14 | } 15 | 16 | interface TransactionTableProps { 17 | loading: boolean; 18 | data: Transaction[]; 19 | } 20 | 21 | const TransactionTable: React.FC = ({ 22 | loading, 23 | data, 24 | }) => { 25 | const getStatusBackgroundColor = (status: string) => { 26 | switch (status) { 27 | case "success": 28 | return { background: "bg-Light-100", text: "text-normal-500" }; 29 | case "failed": 30 | return { background: "bg-light-red", text: "text-dark-red" }; 31 | case "abandoned": 32 | return { background: "bg-yellow-100", text: "text-yellow-500" }; 33 | default: 34 | return { background: "", text: "" }; 35 | } 36 | }; 37 | 38 | return ( 39 |
40 | {loading ? ( 41 |
42 |
43 |
44 | ) : ( 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {data.length > 0 ? ( 57 | data.map((transaction) => { 58 | const { background, text } = getStatusBackgroundColor( 59 | transaction.status 60 | ); 61 | return ( 62 | 63 | 64 | 69 | 76 | 79 | 82 | 83 | ); 84 | }) 85 | ) : ( 86 | 87 | 90 | 91 | )} 92 | 93 |
ContributorAmountStatusCampaignDate
{transaction.customer.email} 65 | 68 | 70 |

73 | {transaction.status} 74 |

75 |
77 | {transaction.subaccount.business_name} 78 | 80 | {new Date(transaction.created_at).toLocaleDateString()} 81 |
88 | No active transactions 89 |
94 | )} 95 |
96 | ); 97 | }; 98 | 99 | export default TransactionTable; 100 | -------------------------------------------------------------------------------- /src/components/campaign/CampaignCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { urlFor } from "../../../supporthive/sanity.cli"; 4 | import { Image } from "../../types/images"; 5 | import NumberFormat from "../../utils/numberFormat"; 6 | 7 | export interface CampaignCardProps { 8 | title: string; 9 | description: string; 10 | goalAmount: number; 11 | raisedAmount: number | null; 12 | daysLeft: number; 13 | images?: Image[]; 14 | _id: string | undefined; 15 | } 16 | 17 | const CampaignCard: React.FC = ({ 18 | title, 19 | description, 20 | goalAmount, 21 | raisedAmount, 22 | daysLeft, 23 | images, 24 | _id, 25 | }) => { 26 | // const progress = (raisedAmount / goalAmount) * 100; 27 | 28 | const isExpired = daysLeft <= 0; 29 | 30 | return ( 31 |
32 | {title} 37 |

{title}

38 |

{description}

39 |
40 |
41 |

Goal Amount

42 | 43 |
44 |
45 |

Raised Amount

46 | 47 |
48 |
49 |

50 | {isExpired ? "Campaign Expired" : `Expires in ${daysLeft} days`} 51 |

52 | 53 | 59 | 60 |
61 | ); 62 | }; 63 | 64 | export default CampaignCard; 65 | -------------------------------------------------------------------------------- /src/components/campaign/CampaignLoader.tsx: -------------------------------------------------------------------------------- 1 | export const CampaignSkeleton = () => { 2 | return ( 3 |
4 |
5 |
6 |
7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/campaign/CreateCampaigns.tsx: -------------------------------------------------------------------------------- 1 | import { FormProvider, useForm } from "react-hook-form"; 2 | import Tabs from "../../UI/TabComponent/tabs"; 3 | import BasicInformation from "../Information/BasicInformation"; 4 | import CampaignInformation from "../Information/CampaignInformation"; 5 | import ContactInformation from "../Information/ContactInformation"; 6 | import { useState } from "react"; 7 | import { campaignSchema, TCampaignSchema } from "../../types/campaign"; 8 | import { zodResolver } from "@hookform/resolvers/zod"; 9 | 10 | const CreateCampaigns = () => { 11 | const [activeTab, setActiveTab] = useState(0); 12 | 13 | const tabFields: (keyof TCampaignSchema)[][] = [ 14 | ["title", "country", "city", "category", "description"], // Basic Information tab fields 15 | [ 16 | "goalAmount", 17 | "startDate", 18 | "endDate", 19 | "raiseMoneyFor", 20 | "importance", 21 | "impact", 22 | "images", 23 | ], // Campaign Information tab fields 24 | ["bank", "accountNumber", "name", "email", "phone"], // Contact Information tab fields 25 | ]; 26 | 27 | const methods = useForm({ 28 | resolver: zodResolver(campaignSchema), 29 | }); 30 | 31 | const handleTabChange = async (index: number) => { 32 | // Validate only the fields for the current active tab 33 | const isValid = await methods.trigger(tabFields[activeTab]); 34 | if (isValid) setActiveTab(index); 35 | }; 36 | 37 | return ( 38 | 39 | 48 | 49 |
50 |
51 | {activeTab === 0 && ( 52 | setActiveTab(1)} /> 53 | )} 54 | {activeTab === 1 && ( 55 | setActiveTab(2)} /> 56 | )} 57 | {activeTab === 2 && } 58 |
59 |
60 |
61 | ); 62 | }; 63 | 64 | export default CreateCampaigns; 65 | -------------------------------------------------------------------------------- /src/components/campaign/alcampaign.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const campaigns = [ 4 | { id: 1, title: "Lagos bank food", description: "Donation to fund for non-governmental..." }, 5 | { id: 2, title: "Help Anita get Surgery", description: "Donation to fund for non-governmental..." }, 6 | { id: 3, title: "Laptop fund for techies", description: "Anita is a 28 year old that cannot afford a..." }, 7 | { id: 4, title: "Help Anita get Surgery", description: "Donation to fund for non-governmental..." }, 8 | { id: 5, title: "Donate - Widows", description: "Anita is a 28 year old that cannot afford a..." } 9 | ]; 10 | 11 | const OngoingCampaigns: React.FC = () => { 12 | return ( 13 |
14 |
15 |

On-going Campaigns

16 | {campaigns.length} 17 |
18 |
    19 | {campaigns.map((campaign) => ( 20 |
  • 21 |

    {campaign.title}

    22 |

    {campaign.description}

    23 |
  • 24 | ))} 25 |
26 |
27 | ); 28 | }; 29 | 30 | export default OngoingCampaigns; 31 | -------------------------------------------------------------------------------- /src/components/donate/donate.tsx: -------------------------------------------------------------------------------- 1 | import { zodResolver } from "@hookform/resolvers/zod"; 2 | import { useForm } from "react-hook-form"; 3 | import { donationSchema, TDonationSchema } from "../../types/donate"; 4 | import { Button } from "../Button"; 5 | import Input from "../Inputs"; 6 | import { fetchCampaignById } from "../../../supporthive/sanity.query"; 7 | import { handlePaymentInitialization } from "../../utils/requests/donate.request"; 8 | import { useParams } from "react-router-dom"; 9 | import { useEffect, useState } from "react"; 10 | import { toast } from "sonner"; 11 | import { useAppSelector } from "../../hook/redux.hook"; 12 | import { RootState } from "../../redux/store"; 13 | 14 | const Donate = () => { 15 | const { 16 | register, 17 | handleSubmit, 18 | reset, 19 | formState: { errors, isSubmitting }, 20 | } = useForm({ resolver: zodResolver(donationSchema) }); 21 | const [subAccount, setSubAccount] = useState(""); 22 | const userDetails = useAppSelector((state: RootState) => state.user); 23 | const sanityID = userDetails.userDetails._id; 24 | const { id } = useParams(); 25 | 26 | useEffect(() => { 27 | const getCampaignDetail = async () => { 28 | try { 29 | const data = await fetchCampaignById(id); 30 | setSubAccount(data.subAccountId); 31 | } catch (error) { 32 | toast.error((error as { message: string }).message); 33 | } 34 | }; 35 | 36 | getCampaignDetail(); 37 | }, [id]); 38 | 39 | const onSubmit = async (data: TDonationSchema) => { 40 | try { 41 | if (sanityID) { 42 | const donationData = { 43 | amount: data.amount, 44 | email: data.email, 45 | subAccountId: subAccount, 46 | userId: sanityID, 47 | campaignId: id, 48 | }; 49 | handlePaymentInitialization(donationData); 50 | reset(); 51 | } 52 | } catch (error) { 53 | toast.error((error as { message: string }).message); 54 | } 55 | }; 56 | return ( 57 |
58 |
59 | 67 | 68 | {errors.amount && ( 69 | {`${errors.amount.message}`} 70 | )} 71 |
72 |
73 | 81 | 82 | {errors.email && ( 83 | {`${errors.email.message}`} 84 | )} 85 |
86 |
87 | 95 |
96 |
97 | ); 98 | }; 99 | 100 | export default Donate; 101 | -------------------------------------------------------------------------------- /src/components/settings/EditProfile.tsx: -------------------------------------------------------------------------------- 1 | import { EditProfileProps } from "../../types/settings"; 2 | import { Button } from "../Button"; 3 | import Input from "../Inputs"; 4 | 5 | const EditProfile = ({ 6 | register, 7 | errors, 8 | handleSubmit, 9 | onSubmit, 10 | isSubmitting, 11 | }: EditProfileProps) => { 12 | return ( 13 |
14 |
15 |
16 | 24 | {errors.firstname && ( 25 | {`${errors.firstname.message}`} 26 | )} 27 |
28 | 29 |
30 | 38 | {errors.lastname && ( 39 | {`${errors.lastname.message}`} 40 | )} 41 |
42 |
43 | 44 |
45 | 53 | 54 | {errors.email && ( 55 | {`${errors.email.message}`} 56 | )} 57 |
58 |
59 | 67 |
68 |
69 | ); 70 | }; 71 | 72 | export default EditProfile; 73 | -------------------------------------------------------------------------------- /src/components/settings/Feedback.tsx: -------------------------------------------------------------------------------- 1 | import { FeedbackProps } from "../../types/settings"; 2 | import { Button } from "../Button"; 3 | 4 | const Feedback = ({ 5 | registerFeedback, 6 | errorsFeedback, 7 | onSubmitFeedback, 8 | isFeedbackSubmitting, 9 | handleSubmitFeedback, 10 | }: FeedbackProps) => { 11 | return ( 12 |
13 |

Send us your feedback

14 |

15 | Bugs? Honest feedback would be appreciated. You can also send new 16 | features you want us to add. 17 |

18 | 19 |
20 |
21 | 27 | 28 | {errorsFeedback.feedback && ( 29 | {`${errorsFeedback.feedback.message}`} 30 | )} 31 |
32 |
33 | 41 |
42 |
43 |
44 | ); 45 | }; 46 | 47 | export default Feedback; 48 | -------------------------------------------------------------------------------- /src/components/settings/Security.tsx: -------------------------------------------------------------------------------- 1 | import { SecurityProps } from "../../types/settings"; 2 | import { Button } from "../Button"; 3 | import Input from "../Inputs"; 4 | 5 | const Security = ({ 6 | registerSecurity, 7 | errorSecurity, 8 | handleSubmitSecurity, 9 | onSecuritySubmit, 10 | isSubmittingSecurity, 11 | }: SecurityProps) => { 12 | return ( 13 |
14 |
15 | 24 | {errorSecurity.currentPassword && ( 25 | {`${errorSecurity.currentPassword.message}`} 26 | )} 27 |
28 | 29 |
30 | 39 | 40 | {errorSecurity.password && ( 41 | {`${errorSecurity.password.message}`} 42 | )} 43 |
44 |
45 | 54 | 55 | {errorSecurity.confirmPassword && ( 56 | {`${errorSecurity.confirmPassword.message}`} 57 | )} 58 |
59 |
60 | 68 |
69 |
70 | ); 71 | }; 72 | 73 | export default Security; 74 | -------------------------------------------------------------------------------- /src/components/spolightCards.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { urlFor } from "../../supporthive/sanity.cli"; 3 | import { Image } from "../types/images"; 4 | import NumberFormat from "../utils/numberFormat"; 5 | 6 | type SpotLightCardProps = { 7 | title: string; 8 | description: string; 9 | goalAmount: number; 10 | images: Image[]; 11 | raisedAmount: number | null; 12 | _id: string; 13 | createdBy: string; 14 | }; 15 | 16 | export const SpotLightCard: React.FC = ({ 17 | title, 18 | description, 19 | goalAmount, 20 | images, 21 | _id, 22 | createdBy, 23 | }) => { 24 | // const amount = raisedAmount && raisedAmount * 100; 25 | // console.log(amount, "amount"); 26 | 27 | // const progressPercentage = ((raisedAmount ?? 0) / goalAmount) * 100; 28 | // console.log(progressPercentage, "progressPercentage"); 29 | 30 | return ( 31 | 32 |
33 | {title} 38 |

{title}

39 |

{description}

40 |

41 | By {createdBy} 42 |

43 | {/* 44 | */} 55 |
56 |

Goal amount -

57 | 61 |
62 |
63 | 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /src/context/createCampaign.context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, ReactNode, useContext, useState } from "react"; 2 | 3 | interface AppContextProps { 4 | showForm: boolean | null; 5 | setShowForm: React.Dispatch>; 6 | } 7 | 8 | export const CampaignFormContext = createContext( 9 | undefined 10 | ); 11 | 12 | export const CampaignFormProvider = ({ children }: { children: ReactNode }) => { 13 | const [showForm, setShowForm] = useState(false); 14 | 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | }; 21 | 22 | // Custom hook to access the app context 23 | export function useAppContext() { 24 | const context = useContext(CampaignFormContext); 25 | 26 | if (!context) { 27 | throw new Error("useApp must be used within an AppProvider"); 28 | } 29 | 30 | return context; 31 | } 32 | -------------------------------------------------------------------------------- /src/context/settings.context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, ReactNode, useContext, useState } from "react"; 2 | 3 | type SettingsTabContextProps = { 4 | tab: number; 5 | setTab: React.Dispatch>; 6 | }; 7 | 8 | export const SettingsTabContext = createContext< 9 | SettingsTabContextProps | undefined 10 | >(undefined); 11 | 12 | export const SettingsTabProvider = ({ children }: { children: ReactNode }) => { 13 | const [tab, setTab] = useState(0); 14 | 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | }; 21 | 22 | export function useSettingsTab() { 23 | const context = useContext(SettingsTabContext); 24 | 25 | if (!context) { 26 | throw new Error("useSettingsTab must be used within an AppProvider"); 27 | } 28 | 29 | return context; 30 | } 31 | -------------------------------------------------------------------------------- /src/context/sidebar.context.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, 3 | ReactNode, 4 | useContext, 5 | useEffect, 6 | useState, 7 | } from "react"; 8 | 9 | interface AppContextProps { 10 | isSideBarOpen: boolean | null; 11 | setIsSideBarOpen: React.Dispatch>; 12 | } 13 | 14 | export const AppContext = createContext(undefined); 15 | 16 | export const AppProvider = ({ children }: { children: ReactNode }) => { 17 | const [isSideBarOpen, setIsSideBarOpen] = useState(true); 18 | useEffect(() => { 19 | const handleResize = () => { 20 | if (window.innerWidth <= 768) { 21 | setIsSideBarOpen(false); 22 | } else { 23 | setIsSideBarOpen(true); 24 | } 25 | }; 26 | 27 | // Initial check 28 | handleResize(); 29 | 30 | // Add event listener 31 | window.addEventListener("resize", handleResize); 32 | 33 | // Clean up the event listener on component unmount 34 | return () => { 35 | window.removeEventListener("resize", handleResize); 36 | }; 37 | }, [setIsSideBarOpen]); 38 | 39 | return ( 40 | 41 | {children} 42 | 43 | ); 44 | }; 45 | 46 | // Custom hook to access the app context 47 | export function useAppContext() { 48 | const context = useContext(AppContext); 49 | 50 | if (!context) { 51 | throw new Error("useApp must be used within an AppProvider"); 52 | } 53 | 54 | return context; 55 | } 56 | -------------------------------------------------------------------------------- /src/context/viewCampaign.context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, ReactNode, useContext, useState } from "react"; 2 | 3 | interface AppContextProps { 4 | viewCampaign: boolean | null; 5 | setViewCampaign: React.Dispatch>; 6 | } 7 | 8 | export const CampaignFormContext = createContext( 9 | undefined 10 | ); 11 | 12 | export const ViewCampaignProvider = ({ children }: { children: ReactNode }) => { 13 | const [viewCampaign, setViewCampaign] = useState(false); 14 | 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | }; 21 | 22 | // Custom hook to access the app context 23 | export function useViewCampaignContext() { 24 | const context = useContext(CampaignFormContext); 25 | 26 | if (!context) { 27 | throw new Error("useApp must be used within an AppProvider"); 28 | } 29 | 30 | return context; 31 | } 32 | -------------------------------------------------------------------------------- /src/firebase.ts: -------------------------------------------------------------------------------- 1 | // Import the functions you need from the SDKs you need 2 | import { initializeApp } from "firebase/app"; 3 | import { getAuth } from "firebase/auth"; 4 | 5 | // TODO: Add SDKs for Firebase products that you want to use 6 | // https://firebase.google.com/docs/web/setup#available-libraries 7 | 8 | // Your web app's Firebase configuration 9 | const firebaseConfig = { 10 | apiKey: process.env.VITE_API_KEY, 11 | authDomain: process.env.VITE_AUTH_DOMAIN, 12 | projectId: process.env.VITE_PROJECT_ID, 13 | storageBucket: process.env.VITE_STORAGE_BUCKET, 14 | messagingSenderId: process.env.VITE_MESSAGING_SENDER_ID, 15 | appId: process.env.VITE_APP_ID, 16 | }; 17 | 18 | // Initialize Firebase 19 | const app = initializeApp(firebaseConfig); 20 | export const auth = getAuth(app); 21 | export default app; 22 | -------------------------------------------------------------------------------- /src/helpers/axiosConfig.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | // SANITY AXIOS INSTANCE 4 | export const Axios = axios.create({ 5 | baseURL: "/api/", 6 | headers: { "X-Custom-Header": "foobar", "Content-Type": "application/json" }, 7 | }); 8 | -------------------------------------------------------------------------------- /src/helpers/config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | key: { 3 | lastPath: "__supporthive_last_path", 4 | userId: "__supporthive_user_id", 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/hook/redux.hook.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from "react-redux"; 2 | import { AppDispatch, RootState } from "../redux/store"; 3 | 4 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 5 | export const useAppDispatch = useDispatch.withTypes(); 6 | export const useAppSelector = useSelector.withTypes(); 7 | -------------------------------------------------------------------------------- /src/icons/DownloadIcon.tsx: -------------------------------------------------------------------------------- 1 | const downloadIcon = () => { 2 | return ( 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default downloadIcon; -------------------------------------------------------------------------------- /src/icons/Facebookicon.tsx: -------------------------------------------------------------------------------- 1 | const FacebookIcon = () => { 2 | return ( 3 | 10 | 14 | 15 | ); 16 | }; 17 | 18 | export default FacebookIcon; 19 | -------------------------------------------------------------------------------- /src/icons/InstagramIcon.tsx: -------------------------------------------------------------------------------- 1 | const InstagramIcon = () => { 2 | return ( 3 | 10 | 11 | 15 | 19 | 23 | 24 | 25 | 26 | 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default InstagramIcon; 39 | -------------------------------------------------------------------------------- /src/icons/LinkedinIcon.tsx: -------------------------------------------------------------------------------- 1 | const LinkedinIcon = () => { 2 | return ( 3 | 10 | 11 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default LinkedinIcon; 31 | -------------------------------------------------------------------------------- /src/icons/TwitterIcon.tsx: -------------------------------------------------------------------------------- 1 | const Twittericon = () => { 2 | return ( 3 | 10 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default Twittericon; 32 | -------------------------------------------------------------------------------- /src/icons/YoutubeIcon.tsx: -------------------------------------------------------------------------------- 1 | const YoutubeIcon = () => { 2 | return ( 3 | 10 | 11 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default YoutubeIcon; 31 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | font-family: "Manrope", sans-serif; 8 | scroll-behavior: smooth; 9 | } 10 | 11 | .logo { 12 | font-family: "Clash Grotesk", sans-serif; 13 | } 14 | 15 | .no-scrollbar::-webkit-scrollbar { 16 | display: none; 17 | } 18 | 19 | .no-scrollbar { 20 | -ms-overflow-style: none; 21 | scrollbar-width: none; 22 | } 23 | 24 | .sidebar { 25 | grid-row: 1 /-1; 26 | } 27 | 28 | .spinner { 29 | width: 24px; 30 | height: 24px; 31 | border-radius: 50%; 32 | padding: 0.5px; 33 | background: conic-gradient(#0000 10%, #ffffff) content-box; 34 | -webkit-mask: repeating-conic-gradient( 35 | #0000 0deg, 36 | #000 1deg 20deg, 37 | #0000 21deg 36deg 38 | ), 39 | radial-gradient( 40 | farthest-side, 41 | #0000 calc(100% - 3.8px), 42 | #000 calc(100% - 3.8px) 43 | ); 44 | -webkit-mask-composite: destination-in; 45 | mask-composite: intersect; 46 | animation: spinner-d55elj 1s infinite steps(10); 47 | } 48 | 49 | .fetchingSpinner { 50 | width: 50px; 51 | height: 50px; 52 | border-radius: 50%; 53 | padding: 0.5px; 54 | background: conic-gradient(#0000 10%, #28a745) content-box; 55 | -webkit-mask: repeating-conic-gradient( 56 | #0000 0deg, 57 | #000 1deg 20deg, 58 | #0000 21deg 36deg 59 | ), 60 | radial-gradient( 61 | farthest-side, 62 | #0000 calc(100% - 3.8px), 63 | #000 calc(100% - 3.8px) 64 | ); 65 | -webkit-mask-composite: destination-in; 66 | mask-composite: intersect; 67 | animation: spinner-d55elj 1s infinite steps(10); 68 | } 69 | 70 | @keyframes spinner-d55elj { 71 | to { 72 | transform: rotate(1turn); 73 | } 74 | } 75 | 76 | /* Hide scrollbar for Chrome, Safari and Opera */ 77 | .no-scrollbar::-webkit-scrollbar { 78 | display: none; 79 | -ms-overflow-style: none; /* IE and Edge */ 80 | scrollbar-width: none; /* Firefox */ 81 | } 82 | 83 | input::-webkit-inner-spin-button, 84 | input::-webkit-outer-spin-button { 85 | -webkit-appearance: none; 86 | margin: 0; 87 | } 88 | 89 | input:-webkit-autofill, 90 | input:-webkit-autofill:hover, 91 | input:-webkit-autofill:focus, 92 | input:-webkit-autofill:active { 93 | -webkit-transition: "color 9999s ease-out, background-color 9999s ease-out"; 94 | -webkit-transition-delay: 9999s; 95 | } 96 | 97 | input:focus { 98 | outline: none; 99 | } 100 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client"; 2 | import { StrictMode } from "react"; 3 | import App from "./App.tsx"; 4 | import "./index.css"; 5 | import ProviderWrapper from "./views/auth/providerWrapper.tsx"; 6 | import { SettingsTabProvider } from "./context/settings.context.tsx"; 7 | import { Toaster } from "sonner"; 8 | import { CampaignFormProvider } from "./context/createCampaign.context.tsx"; 9 | import { ViewCampaignProvider } from "./context/viewCampaign.context.tsx"; 10 | 11 | ReactDOM.createRoot(document.getElementById("root")!).render( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | -------------------------------------------------------------------------------- /src/redux/slices/user.slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import { UserType } from "../../types/user.slice"; 3 | import { RootState } from "../store"; 4 | 5 | // Define a type for the slice state 6 | interface UserDetailsState { 7 | userDetails: UserType; 8 | isLoggedIn: boolean; 9 | } 10 | 11 | // Define the initial state using that type 12 | const initialState: UserDetailsState = { 13 | isLoggedIn: false, // Default value for login state 14 | 15 | userDetails: { 16 | uid: "", 17 | firstname: "", 18 | lastname: "", 19 | email: "", 20 | gender: "", 21 | terms: false, 22 | emailVerified: false, 23 | }, 24 | }; 25 | 26 | // create the slice 27 | 28 | const userSlice = createSlice({ 29 | name: "userDetails", 30 | initialState, 31 | reducers: { 32 | // Action to handle user login and set user details 33 | handleUserDetails: (state, action: PayloadAction) => { 34 | state.isLoggedIn = true; 35 | state.userDetails = action.payload; 36 | }, 37 | 38 | // Action to update user profile while preserving login state 39 | updateUserProfile: (state, action: PayloadAction>) => { 40 | state.userDetails = { 41 | ...state.userDetails, 42 | ...action.payload, // Merge the updated fields with the current state 43 | }; 44 | }, 45 | 46 | logoutUser: (state) => { 47 | state.isLoggedIn = false; // Reset login state 48 | state.userDetails = { 49 | uid: "", 50 | firstname: "", 51 | lastname: "", 52 | email: "", 53 | gender: "", 54 | terms: false, 55 | emailVerified: false, 56 | }; // Clear user details 57 | }, 58 | }, 59 | }); 60 | 61 | export const { handleUserDetails, updateUserProfile, logoutUser } = 62 | userSlice.actions; 63 | 64 | export const userDetails = (state: RootState) => state.user; 65 | 66 | export default userSlice.reducer; 67 | -------------------------------------------------------------------------------- /src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, configureStore } from "@reduxjs/toolkit"; 2 | import storage from "redux-persist/lib/storage"; 3 | import { persistStore, persistReducer } from "redux-persist"; 4 | import userSlice from "./slices/user.slice"; 5 | const persistConfig = { 6 | key: "root", 7 | storage, 8 | }; 9 | 10 | const rootReducer = combineReducers({ 11 | user: userSlice, 12 | }); 13 | 14 | const persistedReducer = persistReducer(persistConfig, rootReducer); 15 | 16 | const store = configureStore({ 17 | reducer: persistedReducer, 18 | middleware: (getDefaultMiddleware) => 19 | getDefaultMiddleware({ 20 | serializableCheck: { 21 | ignoredActions: ["persist/PERSIST", "persist/REHYDRATE"], 22 | }, 23 | }), 24 | }); 25 | 26 | const persistor = persistStore(store); 27 | export { store, persistor }; 28 | 29 | // Infer the `RootState` and `AppDispatch` types from the store itself 30 | export type RootState = ReturnType; 31 | // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} 32 | export type AppDispatch = typeof store.dispatch; 33 | -------------------------------------------------------------------------------- /src/types/auth/createAccount.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const createAccountSchema = z 4 | .object({ 5 | firstname: z 6 | .string({ required_error: "First Name is required" }) 7 | .min(1, "First Name is required"), 8 | lastname: z 9 | .string({ required_error: "Last Name is required" }) 10 | .min(1, "Last Name is required"), 11 | gender: z.enum(["Male", "Female", "Other"], { 12 | required_error: "Gender is required", 13 | }), 14 | email: z.string().email(), 15 | terms: z 16 | .boolean({ required_error: "Terms and Conditions is required" }) 17 | .refine((val) => val === true, { 18 | message: "You must accept the terms and conditions", 19 | }), 20 | password: z.string().min(8, "Password must be at least 8 characters"), 21 | confirmPassword: z 22 | .string() 23 | .min(8, "Confirm Password must be at least 8 characters"), 24 | }) 25 | .refine((data) => data.password === data.confirmPassword, { 26 | message: "Password must match", 27 | path: ["confirmPassword"], 28 | }); 29 | 30 | export type TcreateAccountSchema = z.infer; 31 | -------------------------------------------------------------------------------- /src/types/auth/forgotPassword.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const forgtPasswordSchema = z.object({ 4 | email: z.string().email(), 5 | }); 6 | 7 | export type TforgotPasswordSchema = z.infer; 8 | -------------------------------------------------------------------------------- /src/types/auth/login.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const LoginSchema = z.object({ 4 | email: z.string().email("Invalid email address"), 5 | password: z 6 | .string({ required_error: "Password is required" }) 7 | .min(8, "Password must be at least 8 characters"), 8 | }); 9 | 10 | export type TLogin = z.infer; 11 | -------------------------------------------------------------------------------- /src/types/auth/reset-password.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const ResetPasswordSchema = z 4 | .object({ 5 | newPassword: z 6 | .string() 7 | .min(8, "Password must be at least 8 characters long"), 8 | confirmPassword: z 9 | .string() 10 | .min(8, "Password must be at least 8 characters long"), 11 | }) 12 | .refine((data) => data.newPassword === data.confirmPassword, { 13 | message: "Password must match", 14 | path: ["confirmPassword"], 15 | }); 16 | 17 | export type TResetPassword = z.infer; 18 | -------------------------------------------------------------------------------- /src/types/campaign.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | const MAX_FILE_SIZE = 5000000; 3 | const ACCEPTED_IMAGE_TYPES = [ 4 | "image/jpeg", 5 | "image/jpg", 6 | "image/png", 7 | "image/webp", 8 | ]; 9 | const ACCEPTED_DOCUMENT_TYPES = [ 10 | "application/pdf", 11 | "application/msword", 12 | "application/vnd.openxmlformats-officedocument.wordprocessingml.document", 13 | "application/vnd.ms-powerpoint", 14 | "application/vnd.openxmlformats-officedocument.presentationml.presentation", 15 | ]; 16 | 17 | export const campaignSchema = z.object({ 18 | title: z.string().min(1, "Campaign Title is required"), 19 | country: z.string().min(1, "Campaign Country is required"), 20 | city: z.string().min(1, "Campaign City is required"), 21 | category: z.enum( 22 | [ 23 | "Education", 24 | "Health", 25 | "Emergency Assistance", 26 | "Community Development", 27 | "Career", 28 | "other", 29 | ], 30 | { required_error: "Campaign Category is required" } 31 | ), 32 | description: z.string().min(1, "Campaign Description is required"), 33 | goalAmount: z.string().min(1, "Campaign Goal Amount is required"), 34 | startDate: z.coerce 35 | .date() 36 | .min(new Date(), "Campaign Start Date must be in the future"), 37 | endDate: z.coerce 38 | .date() 39 | .refine( 40 | (val) => val > new Date(), 41 | "Campaign End Date must be after the Start Date" 42 | ), 43 | raiseMoneyFor: z.string().min(50, "must be at least 50 words"), 44 | importance: z.string().min(50, "must be at least 50 words"), 45 | impact: z.string().min(50, "must be at least 50 words"), 46 | images: z 47 | .any() 48 | .refine( 49 | (files) => files?.[0]?.size <= MAX_FILE_SIZE, 50 | `Max image size is 5MB.` 51 | ) 52 | .refine( 53 | (files) => ACCEPTED_IMAGE_TYPES.includes(files?.[0]?.type), 54 | "Only .jpg, .jpeg, .png and .webp formats are supported." 55 | ), 56 | // supportingDocuments: z 57 | // .any() 58 | // .optional() 59 | // .refine( 60 | // (files) => files?.[0]?.size <= MAX_FILE_SIZE, 61 | // `Max document size is 5MB.` 62 | // ) 63 | // .refine( 64 | // (files) => ACCEPTED_DOCUMENT_TYPES.includes(files?.[0]?.type), 65 | // "Only flipdf, doc, docx, ppt, pptx formats are supported." 66 | // ), 67 | name: z.string().min(1, "Name is required"), 68 | email: z.string().email("Invalid email address"), 69 | phone: z.string().min(10, "Phone Number must be at least 10 characters"), 70 | bank: z.string().min(1, "bank code is required"), 71 | accountNumber: z.string().min(1, "account number is required"), 72 | subAccountId: z.string().optional(), 73 | }); 74 | 75 | export type TCampaignSchema = z.infer; 76 | 77 | export type createCampaignProps = { 78 | title: string; 79 | country: string; 80 | city: string; 81 | category: 82 | | "Education" 83 | | "Health" 84 | | "other" 85 | | "Emergency Assistance" 86 | | "Community Development" 87 | | "Career"; 88 | description: string; 89 | goalAmount: string; 90 | startDate: Date; 91 | endDate: Date; 92 | importance: string; 93 | raiseMoneyFor: string; 94 | impact: string; 95 | images: FileList; 96 | supportingDocuments?: FileList; 97 | name: string; 98 | email: string; 99 | phone: string; 100 | userId: string | undefined; 101 | bank: string | undefined; 102 | accountNumber: string | undefined; 103 | subAccountId?: string; 104 | status?: "pending" | "approved" | "rejected"; 105 | }; 106 | 107 | export interface fetchCampaign { 108 | _id: string; 109 | importance: string; 110 | createdBy: { 111 | _id: string; 112 | email: string; 113 | }; 114 | title: string; 115 | category: string; 116 | endDate: string; 117 | impact: string; 118 | status: string; 119 | city: string; 120 | description: string; 121 | startDate: string; 122 | country: string; 123 | goalAmount: any; 124 | raiseMoneyFor: string; 125 | bank: string | undefined; 126 | accountNumber: string | undefined; 127 | subAccountId?: string; 128 | name?: string; 129 | phone?: string; 130 | images?: Image[]; 131 | supportingDocuments?: FileDocument[]; 132 | } 133 | 134 | interface ImageAsset { 135 | _ref: string; 136 | _type: "reference"; 137 | } 138 | 139 | interface Image { 140 | _type: "image"; 141 | _key: string; 142 | asset: ImageAsset; 143 | } 144 | 145 | interface FileAsset { 146 | _ref: string; 147 | _type: "reference"; 148 | } 149 | 150 | interface FileDocument { 151 | _type: "file"; 152 | _key: string; 153 | asset: FileAsset; 154 | } 155 | -------------------------------------------------------------------------------- /src/types/components/button.ts: -------------------------------------------------------------------------------- 1 | export type ButtonProps = { 2 | children: React.ReactNode; 3 | onClick?: (e: React.MouseEvent) => void; 4 | disabled?: boolean; 5 | className?: string; 6 | loading?: boolean; 7 | }; 8 | -------------------------------------------------------------------------------- /src/types/components/input.ts: -------------------------------------------------------------------------------- 1 | export interface InputProps { 2 | label?: string; 3 | name: string; 4 | id: string; 5 | type: string; 6 | additionalClasses?: string; 7 | placeholder?: string; 8 | placeholderStyleOptions?: { [index: string]: boolean | string | number }; 9 | value?: string; 10 | autoComplete: string; 11 | onChange: (e: any) => void; 12 | pattern?: string; 13 | password?: boolean; 14 | additionalAttributes?: { [propName: string]: any }; 15 | select?: boolean; 16 | selectStyleOptions?: { [index: string]: boolean | string | number }; 17 | textarea?: boolean; 18 | rows?: number; 19 | cols?: number; 20 | disabled?: boolean; 21 | required?: boolean; 22 | minLength?: number; 23 | emoji?: boolean; 24 | maxLength?: number; 25 | readOnly?: boolean; 26 | options?: string[]; 27 | optionsKey?: { key: string; value: string }[]; 28 | multiple?: boolean; 29 | accept?: string; 30 | } 31 | -------------------------------------------------------------------------------- /src/types/components/spotlightCampaign.ts: -------------------------------------------------------------------------------- 1 | import { Image } from "../images"; 2 | 3 | export type spotlightCampaign = { 4 | _id: string; 5 | title: string; 6 | country: string; 7 | city: string; 8 | category: string; 9 | description: string; 10 | goalAmount: number; 11 | startDate: string; 12 | endDate: string; 13 | raiseMoneyFor: string; 14 | importance: string; 15 | images: Image[]; 16 | impact: string; 17 | status: string; 18 | createdBy: { 19 | _id: string; 20 | firstname: string; 21 | lastname: string; 22 | email: string; 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/types/createVA.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const createvirtualAccountSchema = z.object({ 4 | first_name: z 5 | .string({ required_error: "First Name is required" }) 6 | .min(1, "First Name is required"), 7 | last_name: z 8 | .string({ required_error: "Last Name is required" }) 9 | .min(1, "Last Name is required"), 10 | email: z.string().email(), 11 | phone: z 12 | .string({ required_error: "First Name is required" }) 13 | .min(1, "First Name is required"), 14 | accountNumber: z 15 | .string() 16 | .min(10, "Account number must be at least 10 digits") 17 | .max(10, "Account number must be at most 10 digits"), 18 | selectedBank: z 19 | .string({ required_error: "Bank is required" }) 20 | .refine((val) => val !== "default", "Please select a valid bank"), 21 | }); 22 | 23 | export type TcreatevirtualAccountSchema = z.infer< 24 | typeof createvirtualAccountSchema 25 | >; 26 | -------------------------------------------------------------------------------- /src/types/donate.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const donationSchema = z.object({ 4 | amount: z.any({ required_error: "Amount is required" }), 5 | 6 | email: z 7 | .string({ 8 | required_error: "Email is required", 9 | }) 10 | .email("Invalid email format"), 11 | }); 12 | 13 | export type TDonationSchema = z.infer; 14 | 15 | export type donateSchema = { 16 | amount: any; 17 | email: string; 18 | subAccountId: string; 19 | userId: string; 20 | campaignId: string | undefined; 21 | }; 22 | -------------------------------------------------------------------------------- /src/types/images.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Image { 3 | _type: "image"; 4 | _key: string; 5 | asset: ImageAsset; 6 | } 7 | export interface ImageAsset { 8 | _ref: string; 9 | _type: "reference"; 10 | } -------------------------------------------------------------------------------- /src/types/settings.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | // update profile schema 4 | 5 | export const updateProfileSchema = z.object({ 6 | firstname: z 7 | .string({ required_error: "First Name is required" }) 8 | .min(1, "First Name is required"), 9 | lastname: z 10 | .string({ required_error: "Last Name is required" }) 11 | .min(1, "Last Name is required"), 12 | 13 | email: z.string().email(), 14 | }); 15 | 16 | export type TupdateProfileSchema = z.infer; 17 | 18 | // security schema 19 | export const securitySchema = z 20 | .object({ 21 | currentPassword: z 22 | .string({ required_error: "Current password is required" }) 23 | .min(1, "Current password is required"), 24 | password: z.string().min(8, "Password must be at least 8 characters"), 25 | confirmPassword: z 26 | .string() 27 | .min(8, "Confirm Password must be at least 8 characters"), 28 | }) 29 | .refine((data) => data.password === data.confirmPassword, { 30 | message: "Password must match", 31 | path: ["confirmPassword"], 32 | }); 33 | 34 | export type TsecuritySchema = z.infer; 35 | 36 | // feedback schema 37 | export const feedbackSchema = z.object({ 38 | feedback: z.string().min(1, "Feedback is required"), 39 | }); 40 | 41 | export type TfeedbackSchema = z.infer; 42 | 43 | // types 44 | export type EditProfileProps = { 45 | register: any; 46 | errors: any; 47 | handleSubmit: any; 48 | onSubmit: any; 49 | isSubmitting: boolean; 50 | }; 51 | 52 | export type SecurityProps = { 53 | registerSecurity: any; 54 | errorSecurity: any; 55 | handleSubmitSecurity: any; 56 | onSecuritySubmit: any; 57 | isSubmittingSecurity: boolean; 58 | }; 59 | 60 | export type FeedbackProps = { 61 | registerFeedback: any; 62 | errorsFeedback: any; 63 | onSubmitFeedback: any; 64 | isFeedbackSubmitting: boolean; 65 | handleSubmitFeedback: any; 66 | }; 67 | -------------------------------------------------------------------------------- /src/types/user.slice.ts: -------------------------------------------------------------------------------- 1 | import { NavigateFunction } from "react-router-dom"; 2 | 3 | export type UserType = { 4 | _id?: string; 5 | uid: string; 6 | firstname: string | undefined; 7 | lastname: string | undefined; 8 | email: string | null; 9 | emailVerified: boolean; 10 | terms: boolean; 11 | gender: string | undefined; 12 | }; 13 | 14 | export type CreateUserOnSanityProps = { 15 | dispatch: any; 16 | userData: UserType; 17 | router: NavigateFunction; 18 | }; 19 | 20 | export type GetUserFromSanityProps = { 21 | dispatch: any; 22 | email: string; 23 | }; 24 | -------------------------------------------------------------------------------- /src/utils/errorMapping.ts: -------------------------------------------------------------------------------- 1 | interface ErrorMessages { 2 | [key: string]: string; 3 | } 4 | 5 | const errorMessages: ErrorMessages = { 6 | "auth/invalid-credential": "Username or password is incorrect.", 7 | "auth/invalid-email": "Invalid email address.", 8 | "auth/email-already-in-use": "Email already in use.", 9 | "auth/user-disabled": "This user has been disabled.", 10 | "auth/user-not-found": "No user found with this email.", 11 | "auth/wrong-password": "Incorrect password. Please try again.", 12 | "auth/weak-password": "Password should be at least 6 characters", 13 | default: "An unknown error occurred. Please try again.", 14 | }; 15 | 16 | const getErrorMessage = (code: string): string => { 17 | return errorMessages[code] || errorMessages["default"]; 18 | }; 19 | 20 | export { getErrorMessage }; 21 | -------------------------------------------------------------------------------- /src/utils/numberFormat.tsx: -------------------------------------------------------------------------------- 1 | import { NumericFormat, NumericFormatProps } from "react-number-format"; 2 | 3 | interface MyNumberFormatProps extends NumericFormatProps { 4 | prefix?: string; 5 | suffix?: string; 6 | decimalScale?: number; 7 | } 8 | 9 | const NumberFormat = ({ 10 | value, 11 | decimalScale, 12 | displayType, 13 | className, 14 | ...otherProps 15 | }: MyNumberFormatProps) => { 16 | return ( 17 | 28 | ); 29 | }; 30 | 31 | export default NumberFormat; 32 | -------------------------------------------------------------------------------- /src/utils/requests/donate.request.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { donateSchema } from "../../types/donate"; 3 | import { toast } from "sonner"; 4 | 5 | export const handlePaymentInitialization = async ( 6 | campaignData: donateSchema 7 | ) => { 8 | const url = "https://api.paystack.co/transaction/initialize"; 9 | const headers = { 10 | Authorization: `Bearer ${process.env.VITE_PAYSTACK_SECRET_KEY}`, 11 | "Content-Type": "application/json", 12 | }; 13 | 14 | const data = { 15 | email: campaignData.email, 16 | amount: campaignData.amount * 100, 17 | subaccount: campaignData.subAccountId, 18 | transaction_charge: 100, 19 | bearer: "subaccount", 20 | callback_url: "https://support-hive.vercel.app/dashboard/transactions", 21 | metadata: { 22 | userId: campaignData.userId, 23 | campaignId: campaignData.campaignId, 24 | }, 25 | }; 26 | 27 | try { 28 | const response = await axios.post(url, data, { headers }); 29 | const { authorization_url } = response.data.data; 30 | 31 | window.location.href = authorization_url; 32 | } catch (error) { 33 | toast.error((error as { message: string }).message); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/utils/requests/transactions.request.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { toast } from "sonner"; 3 | 4 | export const fetchUserTransactions = async (userId: string | undefined) => { 5 | const url = `https://api.paystack.co/transaction`; 6 | const headers = { 7 | Authorization: `Bearer ${process.env.VITE_PAYSTACK_SECRET_KEY}`, 8 | "Content-Type": "application/json", 9 | }; 10 | 11 | try { 12 | const response = await axios.get(url, { 13 | headers, 14 | }); 15 | 16 | const transactions = response.data.data; 17 | const userTransactions = transactions.filter((transaction: any) => { 18 | return transaction.metadata && transaction.metadata.userId === userId; 19 | }); 20 | 21 | return userTransactions; 22 | } catch (error) { 23 | toast.error((error as { message: string }).message); 24 | } 25 | }; 26 | 27 | export const calculateTotalDonationForUser = async ( 28 | userId: string | undefined 29 | ) => { 30 | try { 31 | const userTransactions = await fetchUserTransactions(userId); 32 | 33 | const filterSuccessfulTransactions = userTransactions.filter( 34 | (item: any) => { 35 | return item.status === "success"; 36 | } 37 | ); 38 | 39 | const totalAmount = filterSuccessfulTransactions.reduce( 40 | (acc: number, item: any) => { 41 | return acc + item.amount; 42 | } 43 | ); 44 | const totalAmountInNaira = totalAmount.amount / 100; 45 | 46 | return totalAmountInNaira; 47 | } catch (error) { 48 | toast.error((error as { message: string }).message); 49 | return 0; 50 | } 51 | }; 52 | 53 | export const getTotalDonors = async (userId: string | undefined) => { 54 | try { 55 | const userTransactions = await fetchUserTransactions(userId); 56 | const donors: number = userTransactions.length; 57 | 58 | return donors; 59 | } catch (error) { 60 | toast.error((error as { message: string }).message); 61 | } 62 | }; 63 | 64 | export const calculateTotalAmountForCampaign = async ( 65 | campaignId: string | undefined 66 | ) => { 67 | const url = `https://api.paystack.co/transaction`; 68 | const headers = { 69 | Authorization: `Bearer ${process.env.VITE_PAYSTACK_SECRET_KEY}`, 70 | "Content-Type": "application/json", 71 | }; 72 | 73 | try { 74 | const response = await axios.get(url, { headers }); 75 | const transactions = response.data.data; 76 | 77 | // Filter transactions by campaignId 78 | const campaignTransactions = transactions.filter((transaction: any) => { 79 | return ( 80 | transaction.metadata && 81 | transaction.metadata.campaignId === campaignId && 82 | transaction.status === "success" 83 | ); 84 | }); 85 | 86 | // Calculate total amount for the campaign 87 | const totalAmount = campaignTransactions.reduce( 88 | (acc: number, transaction: any) => { 89 | return acc + transaction.amount; 90 | }, 91 | 0 92 | ); 93 | 94 | return totalAmount / 100; // Convert kobo to Naira 95 | } catch (error) { 96 | toast.error((error as { message: string }).message); 97 | return 0; 98 | } 99 | }; 100 | -------------------------------------------------------------------------------- /src/utils/requests/user.request.ts: -------------------------------------------------------------------------------- 1 | import { toast } from "sonner"; 2 | import { handleUserDetails } from "../../redux/slices/user.slice"; 3 | import { 4 | CreateUserOnSanityProps, 5 | GetUserFromSanityProps, 6 | } from "../../types/user.slice"; 7 | import Cookies from "js-cookie"; 8 | import { config } from "../../helpers/config"; 9 | import { client } from "../../../supporthive/sanity.cli"; 10 | 11 | export const createUserOnSanity = async ({ 12 | userData, 13 | dispatch, 14 | router, 15 | }: CreateUserOnSanityProps) => { 16 | try { 17 | // Create a user document in Sanity 18 | const doc = { 19 | _id: userData._id, 20 | _type: "user", 21 | firstname: userData?.firstname, 22 | lastname: userData?.lastname, 23 | email: userData?.email, 24 | gender: userData?.gender, 25 | uid: userData?.uid, 26 | terms: userData?.terms, 27 | emailVerified: false, // set initial email verification status 28 | }; 29 | 30 | const res = await client.create(doc); 31 | 32 | // Dispatch the user details to Redux 33 | dispatch( 34 | handleUserDetails({ 35 | _id: res._id, 36 | firstname: res.firstname, 37 | lastname: res.lastname, 38 | email: res.email, 39 | uid: res.uid, 40 | gender: res.gender, 41 | terms: res.terms, 42 | emailVerified: res.emailVerified, 43 | }) 44 | ); 45 | 46 | // Handle routing 47 | const getLastPageVisit = Cookies.get(config.key.lastPath); 48 | if (getLastPageVisit) { 49 | router(getLastPageVisit); 50 | } 51 | 52 | // Set user ID cookie 53 | Cookies.set(config.key.userId, res.uid || res._id); // Adjust to your logic 54 | 55 | toast.success("Form submitted successfully"); 56 | return { success: true }; 57 | } catch (error) { 58 | toast.error("Failed to create user. Please try again."); 59 | return { success: false, error }; 60 | } 61 | }; 62 | 63 | export const getUserOnSanity = async ({ 64 | email, 65 | dispatch, 66 | }: GetUserFromSanityProps) => { 67 | // Query to find user by email 68 | const query = `*[_type == "user" && email == $email][0]`; 69 | const params = { email }; 70 | 71 | const user = await client.fetch(query, params); 72 | 73 | if (user) { 74 | // User found, dispatch user details to Redux 75 | dispatch( 76 | handleUserDetails({ 77 | _id: user._id, 78 | firstname: user.firstname, 79 | lastname: user.lastname, 80 | email: user.email, 81 | uid: user.uid, // Adjust if you have a specific UID logic 82 | gender: user.gender, 83 | terms: user.terms, 84 | emailVerified: user.emailVerified, 85 | }) 86 | ); 87 | } 88 | return { success: true }; 89 | }; 90 | -------------------------------------------------------------------------------- /src/utils/userInitials.ts: -------------------------------------------------------------------------------- 1 | export const getInitials = ( 2 | firstname: string | undefined, 3 | lastname: string | undefined 4 | ) => { 5 | const firstInitial = firstname?.charAt(0).toUpperCase(); // Get the first letter of the first name 6 | const lastInitial = lastname?.charAt(0).toUpperCase(); // Get the first letter of the last name 7 | return `${firstInitial}${lastInitial}`; // Combine initials 8 | }; 9 | 10 | // Calculate days left for each campaign 11 | export const calculateDaysLeft = (endDate: number | string) => { 12 | const currentDate = new Date(); 13 | const campaignEndDate = new Date(endDate); 14 | const timeDiff = campaignEndDate.getTime() - currentDate.getTime(); 15 | const daysLeft = Math.ceil(timeDiff / (1000 * 3600 * 24)); // Convert milliseconds to days 16 | return daysLeft; 17 | }; 18 | -------------------------------------------------------------------------------- /src/views/Dashboard/Campaigns.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { fetchCampaign } from "../../types/campaign"; 3 | import { fetchApprovedCampaigns } from "../../../supporthive/sanity.query"; 4 | import { calculateTotalAmountForCampaign } from "../../utils/requests/transactions.request"; 5 | import { Link } from "react-router-dom"; 6 | import { CampaignSkeleton } from "../../components/campaign/CampaignLoader"; 7 | import CampaignCard from "../../components/campaign/CampaignCard"; 8 | import Icon from "../../assets/campaign icon.svg"; 9 | import { calculateDaysLeft } from "../../utils/userInitials"; 10 | 11 | const Campaigns = () => { 12 | const [allCampaigns, setAllCampaigns] = useState([]); 13 | const [loading, setLoading] = useState(false); 14 | const [raisedAmounts, setRaisedAmounts] = useState>( 15 | {} 16 | ); 17 | 18 | useEffect(() => { 19 | const getCampaigns = async () => { 20 | setLoading(true); 21 | const allCampaigns = await fetchApprovedCampaigns(); 22 | 23 | const campaignRaisedAmounts: Record = {}; 24 | for (const campaign of allCampaigns) { 25 | const totalAmount = await calculateTotalAmountForCampaign(campaign._id); 26 | campaignRaisedAmounts[campaign._id] = totalAmount; 27 | } 28 | 29 | setRaisedAmounts(campaignRaisedAmounts); 30 | setAllCampaigns(allCampaigns); 31 | setLoading(false); 32 | }; 33 | 34 | getCampaigns(); 35 | }, []); 36 | 37 | return ( 38 |
39 |
40 |

Campaigns

41 | 42 | 43 | 47 | 48 |
49 | {loading ? ( 50 |
51 | {Array(3) 52 | .fill(null) 53 | .map((_, index) => ( 54 | 55 | ))} 56 |
57 | ) : allCampaigns.length === 0 ? ( 58 |
59 |

No Campaigns Available

60 |
61 | ) : ( 62 |
63 | {allCampaigns.map((campaign) => ( 64 | 74 | ))} 75 |
76 | )} 77 |
78 | ); 79 | }; 80 | 81 | export default Campaigns; 82 | -------------------------------------------------------------------------------- /src/views/Dashboard/Overview.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useAppSelector } from "../../hook/redux.hook"; 3 | import { RootState } from "../../redux/store"; 4 | import { toast } from "sonner"; 5 | import { 6 | calculateTotalDonationForUser, 7 | getTotalDonors, 8 | } from "../../utils/requests/transactions.request"; 9 | import NumberFormat from "../../utils/numberFormat"; 10 | import { getTotalCampaigns } from "../../utils/requests/campaign.request"; 11 | import { fetchApprovedCampaigns } from "../../../supporthive/sanity.query"; 12 | import { Link } from "react-router-dom"; 13 | 14 | type Campaign = { 15 | _id: string; 16 | title: string; 17 | description: string; 18 | }; 19 | 20 | export const OverviewCards = ({ 21 | title, 22 | amount, 23 | number, 24 | }: { 25 | title: string; 26 | amount?: number | null; 27 | number?: number | null; 28 | }) => { 29 | return ( 30 |
31 |

{title}

32 | 33 | {amount ? ( 34 | 38 | ) : ( 39 |

{number ?? "0"}

40 | )} 41 |
42 | ); 43 | }; 44 | const Overview = () => { 45 | const [totalAmount, setTotalAmount] = useState(); 46 | const [totalDonors, setTotalDonors] = useState(); 47 | const [totalCampaigns, setTotalCampaigns] = useState(); 48 | const [loading, setLoading] = useState(true); 49 | const [approvedCampaigns, setApprovedCampaigns] = useState([]); 50 | 51 | const userDetails = useAppSelector((state: RootState) => state.user); 52 | const userId = userDetails.userDetails._id; 53 | 54 | useEffect(() => { 55 | const getTotalAmount = async () => { 56 | try { 57 | if (userId) { 58 | const response = await Promise.allSettled([ 59 | calculateTotalDonationForUser(userId), 60 | getTotalDonors(userId), 61 | getTotalCampaigns(userId), 62 | ]); 63 | setTotalAmount( 64 | response[0].status === "fulfilled" ? response[0].value : undefined 65 | ); 66 | setTotalDonors( 67 | response[1].status === "fulfilled" ? response[1].value : undefined 68 | ); 69 | setTotalCampaigns( 70 | response[2].status === "fulfilled" ? response[2].value : undefined 71 | ); 72 | } 73 | } catch (error) { 74 | toast.error((error as { message: string }).message); 75 | } finally { 76 | setLoading(false); 77 | } 78 | }; 79 | 80 | getTotalAmount(); 81 | }, [userId]); 82 | 83 | useEffect(() => { 84 | const getApprovedCampaigns = async () => { 85 | setLoading(true); 86 | const campaigns = await fetchApprovedCampaigns(); 87 | setApprovedCampaigns(campaigns.slice(0, 3)); 88 | setLoading(false); 89 | }; 90 | 91 | getApprovedCampaigns(); 92 | }, []); 93 | 94 | return ( 95 |
96 |
97 |
98 |
99 |

Dashboard

100 |

101 | Get an overview of your account! 102 |

103 |
104 |
105 | 106 |
107 | 108 | 109 | 110 |
111 |
112 | 113 |
114 |
115 |
116 |

On-going Campaigns

117 |
118 | {approvedCampaigns.length} 119 |
120 |
121 | 122 |
123 | {loading ? ( 124 |
125 | {Array(3) 126 | .fill(null) 127 | .map((_, index) => ( 128 |
132 |

133 |

134 |
135 | ))} 136 |
137 | ) : ( 138 | approvedCampaigns.map((campaign) => ( 139 | 143 |
144 |

145 | {campaign.title} 146 |

147 |

148 | {campaign.description} 149 |

150 |
151 | 152 | )) 153 | )} 154 |
155 |
156 |
157 |
158 | ); 159 | }; 160 | 161 | export default Overview; 162 | -------------------------------------------------------------------------------- /src/views/Dashboard/Transactions.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { fetchUserTransactions } from "../../utils/requests/transactions.request"; 3 | import TransactionTable from "../../components/Transaction/table"; 4 | import { useAppSelector } from "../../hook/redux.hook"; 5 | import { RootState } from "../../redux/store"; 6 | import { toast } from "sonner"; 7 | 8 | interface Transaction { 9 | id: number; 10 | status: string; 11 | amount: number; 12 | customer: { 13 | email: string; 14 | }; 15 | subaccount: { 16 | business_name: string; 17 | }; 18 | created_at: string; 19 | } 20 | 21 | const Transactions = () => { 22 | const userDetails = useAppSelector((state: RootState) => state.user); 23 | const sanityID = userDetails.userDetails._id; 24 | 25 | const [transactions, setTransactions] = useState([]); 26 | const [loading, setLoading] = useState(true); 27 | const [error, setError] = useState(""); 28 | 29 | useEffect(() => { 30 | const getTransactions = async () => { 31 | setLoading(true); 32 | 33 | try { 34 | if (sanityID) { 35 | const transactionList = await fetchUserTransactions(sanityID); 36 | setTransactions(transactionList); 37 | } 38 | } catch (error) { 39 | setError("Failed to fetch transactions."); 40 | toast.error((error as { message: string }).message); 41 | } finally { 42 | setLoading(false); 43 | } 44 | }; 45 | 46 | getTransactions(); 47 | }, []); 48 | return ( 49 |
50 |
51 |
52 |

Transactions

53 |

54 | Keep track of donations made on your campaigns! 55 |

56 |
57 |
58 | 59 | 60 | {error &&

{error}

} 61 |
62 | ); 63 | }; 64 | 65 | export default Transactions; 66 | -------------------------------------------------------------------------------- /src/views/Dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | import Sidebar from "../../components/Sidebar"; 2 | import Topbar from "../../components/Topbar"; 3 | import { Outlet, useNavigate } from "react-router-dom"; 4 | import { AppProvider } from "../../context/sidebar.context"; 5 | import { useEffect } from "react"; 6 | import { auth } from "../../firebase"; 7 | 8 | const Dashboardlayout = () => { 9 | const navigate = useNavigate(); 10 | 11 | useEffect(() => { 12 | const user = auth.currentUser; 13 | 14 | if (user && !user.emailVerified) { 15 | navigate("/email-verification"); 16 | } 17 | }, [navigate]); 18 | return ( 19 | 20 |
21 | 22 |
23 | 24 |
25 | 26 |
27 |
28 |
29 |
30 | ); 31 | }; 32 | 33 | export default Dashboardlayout; 34 | -------------------------------------------------------------------------------- /src/views/ErrorPage.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import Error from "../assets/error.svg"; 3 | 4 | const ErrorPage = () => { 5 | return ( 6 |
7 |
8 | 9 |

Something went wrong.

10 |

11 | Sorry, we can’t find the page you’re looking for. 12 |

13 | 14 | 17 | 18 |
19 |
20 | ); 21 | }; 22 | 23 | export default ErrorPage; 24 | -------------------------------------------------------------------------------- /src/views/LandingPage/HeroSection.tsx: -------------------------------------------------------------------------------- 1 | const HeroSection = () => { 2 | return ( 3 |
4 |
5 | background image 10 | background image 15 |
16 | 17 |
18 | {/* Small Button at the top */} 19 |
20 | 23 |
24 | 25 | {/* Main Title */} 26 |

27 | CROWDFUNDING WORLDWIDE 28 |

29 | 30 | {/* Subtitle */} 31 |

32 | Discover an easy and effective way to raise funds for personal 33 | initiatives, charitable projects, and friends in need. 34 |

35 | 36 | {/* Search Input */} 37 | {/*
38 |
39 | 46 | 52 | 53 | 58 | 61 |
62 |
*/} 63 |
64 |
65 |
66 | ); 67 | }; 68 | 69 | export default HeroSection; 70 | -------------------------------------------------------------------------------- /src/views/LandingPage/How.tsx: -------------------------------------------------------------------------------- 1 | import People from "../../assets/people icon.svg"; 2 | import Fund from "../../assets/fund icon.svg"; 3 | import Message from "../../assets/message icon.svg"; 4 | 5 | const How = () => { 6 | const data = [ 7 | { 8 | img: Message, 9 | title: "Get started", 10 | description: 11 | "SupportHive helps you along the way, so you can focus on making great things", 12 | }, 13 | { 14 | img: People, 15 | title: "Create your campaign", 16 | description: 17 | "Tell your story, set your goal, and download our app to get started", 18 | }, 19 | { 20 | img: Fund, 21 | title: "Receive funding", 22 | description: 23 | "Share your campaign and get funded by your friends and family", 24 | }, 25 | ]; 26 | return ( 27 |
28 |

29 | How it works 30 |

31 | 32 |
33 | {data.map((item, index) => ( 34 |
38 | icon 39 |

40 | {item.title}{" "} 41 |

42 |

{item.description}

43 |
44 | ))} 45 |
46 |
47 | ); 48 | }; 49 | 50 | export default How; 51 | -------------------------------------------------------------------------------- /src/views/LandingPage/Mobilize.tsx: -------------------------------------------------------------------------------- 1 | import Charity from "../../assets/charity (2).svg"; 2 | import Community from "../../assets/community (2).svg"; 3 | import Academic from "../../assets/academic.svg"; 4 | import Disaster from "../../assets/disaster.svg"; 5 | import Legal from "../../assets/legal.svg"; 6 | import Medical from "../../assets/medical.svg"; 7 | 8 | const Mobilize = () => { 9 | const data = [ 10 | { 11 | img: Charity, 12 | title: "Charity", 13 | }, 14 | { 15 | img: Community, 16 | title: "Community", 17 | }, 18 | { 19 | img: Academic, 20 | title: "Academic", 21 | }, 22 | { 23 | img: Disaster, 24 | title: "Disaster", 25 | }, 26 | { 27 | img: Legal, 28 | title: "Legal", 29 | }, 30 | { 31 | img: Medical, 32 | title: "Medical", 33 | }, 34 | ]; 35 | return ( 36 |
37 |

38 | Mobilize Resources for 39 |

40 |
41 | {data.map((item, index) => ( 42 |
46 | {item.title} 47 |
48 | ))} 49 |
50 |
51 | ); 52 | }; 53 | 54 | export default Mobilize; 55 | -------------------------------------------------------------------------------- /src/views/LandingPage/Spotlight.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { fetchApprovedCampaigns } from "../../../supporthive/sanity.query"; 3 | import { CampaignSkeleton } from "../../components/campaign/CampaignLoader"; 4 | import { SpotLightCard } from "../../components/spolightCards"; 5 | import { calculateTotalAmountForCampaign } from "../../utils/requests/transactions.request"; 6 | import { spotlightCampaign } from "../../types/components/spotlightCampaign"; 7 | 8 | const Spotlight = () => { 9 | const [approvedCampaigns, setApprovedCampaigns] = useState< 10 | spotlightCampaign[] 11 | >([]); 12 | const [loading, setLoading] = useState(false); 13 | const [totalAmountForCampaign, setTotalAmountForCampaign] = useState< 14 | number | null 15 | >(null); 16 | 17 | useEffect(() => { 18 | const getApprovedCampaigns = async () => { 19 | setLoading(true); 20 | const campaigns = await fetchApprovedCampaigns(); 21 | setApprovedCampaigns(campaigns); 22 | const totalAmount = await calculateTotalAmountForCampaign(campaigns._id); 23 | setTotalAmountForCampaign(totalAmount); 24 | 25 | setLoading(false); 26 | }; 27 | 28 | getApprovedCampaigns(); 29 | }, []); 30 | 31 | return ( 32 |
36 |

In the Spotlight

37 | {loading ? ( 38 |
39 | {Array(3) 40 | .fill(null) 41 | .map((_, index) => ( 42 | 43 | ))} 44 |
45 | ) : ( 46 |
47 | {approvedCampaigns.length > 0 ? ( 48 | approvedCampaigns 49 | .slice(0, 3) 50 | .map((campaign) => ( 51 | 65 | )) 66 | ) : ( 67 |

No campaigns available.

68 | )} 69 |
70 | )} 71 |
72 | ); 73 | }; 74 | 75 | export default Spotlight; 76 | -------------------------------------------------------------------------------- /src/views/LandingPage/contact.tsx: -------------------------------------------------------------------------------- 1 | import { MdMail } from "react-icons/md"; 2 | 3 | const Contact = () => { 4 | return ( 5 |
6 |
7 |

Contact

8 |

9 | Get in Touch With Us 10 |

11 |

12 | 3rd Floor, Nova Building, Off Awolowo Road, Ikoyi, Lagos. 13 |

14 |
15 | 16 | support@supporthive.com 17 |
18 |
19 |
20 | ); 21 | }; 22 | 23 | export default Contact; 24 | -------------------------------------------------------------------------------- /src/views/LandingPage/index.tsx: -------------------------------------------------------------------------------- 1 | import Footer from "../../components/Footer"; 2 | import NavBar from "../../components/Nav"; 3 | import Contact from "./contact"; 4 | import HeroSection from "./HeroSection"; 5 | import How from "./How"; 6 | import Mobilize from "./Mobilize"; 7 | import Spotlight from "./Spotlight"; 8 | 9 | const LandingPage = () => { 10 | return ( 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 | ); 23 | }; 24 | 25 | export default LandingPage; 26 | -------------------------------------------------------------------------------- /src/views/auth/AuthSidebar.tsx: -------------------------------------------------------------------------------- 1 | import Imagedndnd from "../../assets/union.svg"; 2 | 3 | const AuthSidebar = () => { 4 | return ( 5 |
6 |
7 |

8 | Empowering Dreams, One Contribution at a Time{" "} 9 |

10 |

11 | Together, we can fund the future we believe in.{" "} 12 |

13 | 14 | support-img 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default AuthSidebar; 25 | -------------------------------------------------------------------------------- /src/views/auth/EmailVerification.tsx: -------------------------------------------------------------------------------- 1 | import { Link, useNavigate } from "react-router-dom"; 2 | import AuthLayout from "./Layout"; 3 | import { Button } from "../../components/Button"; 4 | import { sendEmailVerification } from "firebase/auth"; 5 | import { auth } from "../../firebase"; 6 | import { toast } from "sonner"; 7 | import { useEffect } from "react"; 8 | 9 | const EmailVerification = () => { 10 | const handleResendLink = async () => { 11 | const user = auth.currentUser; 12 | 13 | if (user) { 14 | try { 15 | await sendEmailVerification(user); 16 | toast.success("Verification email sent! Check your inbox."); 17 | } catch (error) { 18 | toast.error((error as { message: string }).message); 19 | } 20 | } 21 | }; 22 | 23 | const navigate = useNavigate(); 24 | 25 | useEffect(() => { 26 | const user = auth.currentUser; 27 | 28 | if (user && user.emailVerified) { 29 | navigate("/login"); 30 | } 31 | }, [navigate]); 32 | 33 | return ( 34 | 35 |
36 |
37 |

Email Verification

38 |

39 | Activate your account by verifying your email address{" "} 40 |

41 |
42 |
43 |

44 | Your account was created successfully{" "} 45 |

46 |
47 | 48 |

49 | Thanks for signing up! Please check your email and click the 50 | activation link to complete your account setup.{" "} 51 |

52 | 53 |
54 | 60 |
61 |

Already have an account?

62 | 63 | {" "} 64 | Log in 65 | 66 |
67 |
68 |
69 |
70 | ); 71 | }; 72 | 73 | export default EmailVerification; 74 | -------------------------------------------------------------------------------- /src/views/auth/ForgotPassword.tsx: -------------------------------------------------------------------------------- 1 | import { useForm } from "react-hook-form"; 2 | import { Button } from "../../components/Button"; 3 | import Input from "../../components/Inputs"; 4 | import AuthLayout from "./Layout"; 5 | import { zodResolver } from "@hookform/resolvers/zod"; 6 | import { 7 | forgtPasswordSchema, 8 | TforgotPasswordSchema, 9 | } from "../../types/auth/forgotPassword"; 10 | import { auth } from "../../firebase"; 11 | import { toast } from "sonner"; 12 | import { Link, useNavigate } from "react-router-dom"; 13 | import { sendPasswordResetEmail } from "firebase/auth"; 14 | 15 | const ForgotPassword = () => { 16 | const { 17 | register, 18 | handleSubmit, 19 | reset, 20 | formState: { errors, isSubmitting }, 21 | } = useForm({ 22 | resolver: zodResolver(forgtPasswordSchema), 23 | }); 24 | 25 | const navigate = useNavigate(); 26 | const onSubmit = async (data: TforgotPasswordSchema) => { 27 | try { 28 | await sendPasswordResetEmail(auth, data.email); 29 | reset(); 30 | toast.success("Check your email for the password reset link!"); 31 | navigate("/reset-password-confirm"); // Redirect to reset-password confirm screen 32 | } catch (error) { 33 | toast.error((error as { message: string }).message); 34 | } 35 | }; 36 | 37 | return ( 38 | 39 |
40 |
41 |

Forgot Password?

42 |

43 | Please enter your registered email address to reset your password 44 |

45 |
46 | 47 |
48 |
49 | 57 | 58 | {errors.email && ( 59 | {`${errors.email.message}`} 60 | )} 61 |
62 | 63 |
64 | 72 |
73 |
74 | 75 | {" "} 76 | Back to login 77 | 78 |
79 |
80 | ); 81 | }; 82 | 83 | export default ForgotPassword; 84 | -------------------------------------------------------------------------------- /src/views/auth/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import AuthSidebar from "./AuthSidebar"; 3 | 4 | const AuthLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { 5 | return ( 6 |
7 |
8 |
9 |
10 | 11 | Logo 12 | 13 |
14 | 15 | {children} 16 |
17 |
18 |
19 | 20 |
21 |
22 | ); 23 | }; 24 | 25 | export default AuthLayout; 26 | -------------------------------------------------------------------------------- /src/views/auth/Login.tsx: -------------------------------------------------------------------------------- 1 | import { Link, useNavigate } from "react-router-dom"; 2 | import AuthLayout from "./Layout"; 3 | import { Button } from "../../components/Button"; 4 | import Input from "../../components/Inputs"; 5 | import { useForm } from "react-hook-form"; 6 | import { LoginSchema, TLogin } from "../../types/auth/login"; 7 | import { zodResolver } from "@hookform/resolvers/zod"; 8 | import { signInWithEmailAndPassword } from "firebase/auth"; 9 | import { auth } from "../../firebase"; 10 | import { toast } from "sonner"; 11 | import { getUserOnSanity } from "../../utils/requests/user.request"; 12 | import { useAppDispatch } from "../../hook/redux.hook"; 13 | import { config } from "../../helpers/config"; 14 | import Cookies from "js-cookie"; 15 | import { getErrorMessage } from "../../utils/errorMapping"; 16 | 17 | const Login = () => { 18 | const navigate = useNavigate(); 19 | const dispatch = useAppDispatch(); 20 | const { 21 | register, 22 | handleSubmit, 23 | reset, 24 | formState: { errors, isSubmitting }, 25 | } = useForm({ resolver: zodResolver(LoginSchema) }); 26 | 27 | const onSubmit = async (data: TLogin) => { 28 | const { email, password } = data; 29 | try { 30 | const userCredential = await signInWithEmailAndPassword( 31 | auth, 32 | email, 33 | password 34 | ); 35 | 36 | const user = userCredential.user; 37 | const { uid } = user; 38 | 39 | if (uid) { 40 | const sanityFetched = await getUserOnSanity({ email, dispatch }); 41 | if (sanityFetched?.success) { 42 | toast.success("Login Successful"); 43 | reset(); 44 | Cookies.set(config.key.userId, uid); 45 | const getLastPageVisit = Cookies.get(config.key.lastPath); 46 | if (getLastPageVisit) { 47 | navigate(getLastPageVisit); 48 | return; 49 | } 50 | navigate("/dashboard/overview"); 51 | } 52 | } 53 | } catch (err: any) { 54 | const errMsg = getErrorMessage(err?.code); 55 | toast.error(errMsg); 56 | } 57 | }; 58 | 59 | return ( 60 | 61 |
62 |
63 |

Welcome back!

64 |

Log in to begin and manage your campaign

65 |
66 | 67 |
68 |
69 |
70 | 78 | 79 | {errors.email && ( 80 | {`${errors.email.message}`} 81 | )} 82 |
83 |
84 | 93 | 94 | {errors.password && ( 95 | {`${errors.password.message}`} 96 | )} 97 |
98 | 99 | 102 |
103 | 104 |
105 | 113 | 114 |
115 |

Don't have an account?

116 | 117 | {" "} 118 | Create Account 119 | 120 |
121 |
122 |
123 |
124 |
125 | ); 126 | }; 127 | 128 | export default Login; 129 | -------------------------------------------------------------------------------- /src/views/auth/ProtectedRoute.tsx: -------------------------------------------------------------------------------- 1 | // ProtectedRoute.tsx 2 | import { Navigate } from "react-router-dom"; 3 | import { useSelector } from "react-redux"; 4 | import { RootState } from "../../redux/store"; 5 | 6 | const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ 7 | children, 8 | }) => { 9 | const isLoggedIn = useSelector((state: RootState) => state.user.isLoggedIn); // Adjust based on your Redux state 10 | 11 | if (!isLoggedIn) { 12 | return ; 13 | } 14 | 15 | return <>{children}; 16 | }; 17 | 18 | export default ProtectedRoute; 19 | -------------------------------------------------------------------------------- /src/views/auth/ResetPassword.tsx: -------------------------------------------------------------------------------- 1 | import { Link, useLocation, useNavigate } from "react-router-dom"; 2 | import { Button } from "../../components/Button"; 3 | import Input from "../../components/Inputs"; 4 | import AuthLayout from "./Layout"; 5 | import { useForm } from "react-hook-form"; 6 | import { zodResolver } from "@hookform/resolvers/zod"; 7 | import { 8 | ResetPasswordSchema, 9 | TResetPassword, 10 | } from "../../types/auth/reset-password"; 11 | import { toast } from "sonner"; 12 | import { auth } from "../../firebase"; 13 | import { confirmPasswordReset } from "firebase/auth"; 14 | 15 | const ResetPassword = () => { 16 | const { 17 | register, 18 | handleSubmit, 19 | reset, 20 | formState: { errors, isSubmitting }, 21 | } = useForm({ resolver: zodResolver(ResetPasswordSchema) }); 22 | 23 | const location = useLocation(); 24 | const navigate = useNavigate(); 25 | 26 | // Parse the query string to get the oobCode 27 | const queryParams = new URLSearchParams(location.search); 28 | const oobCode = queryParams.get("oobCode"); 29 | 30 | const onSubmit = async (data: TResetPassword) => { 31 | if (oobCode) { 32 | try { 33 | await confirmPasswordReset(auth, oobCode, data.newPassword); 34 | reset(); 35 | toast.success("Password reset successfully! You can now log in."); 36 | navigate("/login"); // Redirect to your login page or wherever you prefer 37 | } catch (error) { 38 | toast.error((error as { message: string }).message); 39 | } 40 | } 41 | }; 42 | return ( 43 | 44 |
45 |
46 |

Reset Password

47 |

Log in to begin and manage your campaign

48 |
49 | 50 |
51 |
52 |
53 | 62 | {errors.newPassword && ( 63 | {`${errors.newPassword.message}`} 64 | )} 65 |
66 |
67 | 76 | {errors.confirmPassword && ( 77 | {`${errors.confirmPassword.message}`} 78 | )} 79 |
80 | 83 |
84 | 85 |
86 | 94 |
95 |

Don't have an account?

96 | 97 | {" "} 98 | Create Account 99 | 100 |
101 |
102 |
103 |
104 |
105 | ); 106 | }; 107 | 108 | export default ResetPassword; 109 | -------------------------------------------------------------------------------- /src/views/auth/ResetPasswordConfirm.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import AuthLayout from "./Layout"; 3 | import { Button } from "../../components/Button"; 4 | import { auth } from "../../firebase"; 5 | import { toast } from "sonner"; 6 | import { sendPasswordResetEmail } from "firebase/auth"; 7 | 8 | const ResetPasswordConfirm = () => { 9 | const user = auth.currentUser; 10 | const email = user ? user.email : null; 11 | 12 | const handleResendLink = async () => { 13 | if (email) { 14 | try { 15 | await sendPasswordResetEmail(auth, email); 16 | toast.success("Check your email for the password reset link!"); 17 | } catch (error) { 18 | toast.error((error as { message: string }).message); 19 | } 20 | } 21 | }; 22 | return ( 23 | 24 |
25 |
26 |

Forgot Password

27 |

Don't panic, it happens to most of us.

28 |
29 | 30 |
31 |

32 | Reset password link sent successfully 33 |

34 |
35 | 36 |

37 | A link to reset your password has been sent to your email 38 |

39 |
40 | 46 |
47 |

Already have an account?

48 | 49 | {" "} 50 | Log in 51 | 52 |
53 |
54 |
55 |
56 | ); 57 | }; 58 | 59 | export default ResetPasswordConfirm; 60 | -------------------------------------------------------------------------------- /src/views/auth/VerificationSuccessful.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import AuthLayout from "./Layout"; 3 | 4 | const VerificationSuccessful = () => { 5 | return ( 6 | 7 |
8 |
9 |

Email Verification

10 |

11 | Activate your account by verifying your email address{" "} 12 |

13 |
14 |
15 |

Verification Successful

16 |
17 | 18 |

19 | Your account is verified and activated. You can now log in.{" "} 20 |

21 | 22 |
23 |

Already have an account?

24 | 25 | {" "} 26 | Log in 27 | 28 |
29 |
30 |
31 | ); 32 | }; 33 | 34 | export default VerificationSuccessful; 35 | -------------------------------------------------------------------------------- /src/views/auth/providerWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Provider } from "react-redux"; 3 | import { persistor, store } from "../../redux/store"; 4 | import { PersistGate } from "redux-persist/integration/react"; 5 | 6 | const ProviderWrapper = ({ children }: { children: React.ReactNode }) => { 7 | return ( 8 | 9 | 10 | {children} 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default ProviderWrapper; 17 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // vite-env.d.ts 4 | interface ImportMetaEnv { 5 | readonly VITE_API_KEY: string; 6 | readonly VITE_AUTH_DOMAIN: string; 7 | readonly VITE_PROJECT_ID: string; 8 | readonly VITE_STORAGE_BUCKET: string; 9 | readonly VITE_MESSAGING_SENDER_ID: string; 10 | readonly VITE_APP_ID: string; 11 | } 12 | 13 | interface ImportMeta { 14 | readonly env: ImportMetaEnv; 15 | } 16 | -------------------------------------------------------------------------------- /supporthive/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sanity/eslint-config-studio" 3 | } 4 | -------------------------------------------------------------------------------- /supporthive/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # Compiled Sanity Studio 9 | /dist 10 | 11 | # Temporary Sanity runtime, generated by the CLI on every dev server start 12 | /.sanity 13 | 14 | # Logs 15 | /logs 16 | *.log 17 | 18 | # Coverage directory used by testing tools 19 | /coverage 20 | 21 | # Misc 22 | .DS_Store 23 | *.pem 24 | 25 | # Typescript 26 | *.tsbuildinfo 27 | 28 | # Dotenv and similar local-only files 29 | *.local 30 | -------------------------------------------------------------------------------- /supporthive/README.md: -------------------------------------------------------------------------------- 1 | # Sanity Clean Content Studio 2 | 3 | Congratulations, you have now installed the Sanity Content Studio, an open-source real-time content editing environment connected to the Sanity backend. 4 | 5 | Now you can do the following things: 6 | 7 | - [Read “getting started” in the docs](https://www.sanity.io/docs/introduction/getting-started?utm_source=readme) 8 | - [Join the community Slack](https://slack.sanity.io/?utm_source=readme) 9 | - [Extend and build plugins](https://www.sanity.io/docs/content-studio/extending?utm_source=readme) 10 | -------------------------------------------------------------------------------- /supporthive/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supporthive", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "package.json", 6 | "license": "UNLICENSED", 7 | "scripts": { 8 | "dev": "sanity dev", 9 | "start": "sanity start", 10 | "build": "sanity build", 11 | "deploy": "sanity deploy", 12 | "deploy-graphql": "sanity graphql deploy" 13 | }, 14 | "keywords": [ 15 | "sanity" 16 | ], 17 | "dependencies": { 18 | "@sanity/vision": "^3.61.0", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "sanity": "^3.61.0", 22 | "styled-components": "^6.1.8" 23 | }, 24 | "devDependencies": { 25 | "@sanity/eslint-config-studio": "^4.0.0", 26 | "@types/react": "^18.0.25", 27 | "eslint": "^8.6.0", 28 | "prettier": "^3.0.2", 29 | "typescript": "^5.1.6" 30 | }, 31 | "prettier": { 32 | "semi": false, 33 | "printWidth": 100, 34 | "bracketSpacing": false, 35 | "singleQuote": true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /supporthive/sanity.cli.ts: -------------------------------------------------------------------------------- 1 | import {createClient, type ClientConfig} from '@sanity/client' 2 | import imageUrlBuilder from '@sanity/image-url' 3 | 4 | const config: ClientConfig = { 5 | projectId: 'js440jqn', 6 | dataset: 'production', 7 | apiVersion: '2024-03-16', 8 | useCdn: false, 9 | token: process.env.VITE_SANITY_TOKEN, 10 | ignoreBrowserTokenWarning: true, 11 | } 12 | 13 | export const client = createClient(config) 14 | 15 | const builder = imageUrlBuilder(client) 16 | 17 | type SourceType = 18 | | string 19 | | { 20 | _type: 'image' 21 | asset: { 22 | _ref: string 23 | _type: 'reference' 24 | } 25 | } 26 | 27 | export const urlFor = (source: SourceType) => builder.image(source).auto('format').url() 28 | -------------------------------------------------------------------------------- /supporthive/sanity.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'sanity' 2 | import {structureTool} from 'sanity/structure' 3 | import {visionTool} from '@sanity/vision' 4 | import {schemaTypes} from './schemaTypes' 5 | 6 | export default defineConfig({ 7 | name: 'default', 8 | title: 'supportHive', 9 | 10 | projectId: 'js440jqn', 11 | dataset: 'production', 12 | 13 | plugins: [structureTool(), visionTool()], 14 | 15 | schema: { 16 | types: schemaTypes, 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /supporthive/sanity.query.ts: -------------------------------------------------------------------------------- 1 | import {client} from './sanity.cli' 2 | import {toast} from 'sonner' 3 | 4 | const userQuery = `*[_type == "user" && uid == $uid][0] { 5 | _id, 6 | uid, 7 | firstname, 8 | lastname, 9 | email, 10 | emailVerified, 11 | }` 12 | 13 | // Get user Details 14 | export const getUserDetails = async (uid: string) => { 15 | const params = {uid} 16 | const user = await client.fetch(userQuery, params) 17 | return user 18 | } 19 | 20 | export const fetchAllCampaigns = async () => { 21 | const query = `*[_type == "campaign"]{ 22 | _id, 23 | title, 24 | country, 25 | city, 26 | category, 27 | description, 28 | goalAmount, 29 | startDate, 30 | endDate, 31 | raiseMoneyFor, 32 | importance, 33 | impact, 34 | status, 35 | subAccountId, 36 | bank, 37 | images, 38 | supportingDocuments, 39 | accountNumber, 40 | createdBy->{ 41 | _id, 42 | firstname, 43 | lastname, 44 | email 45 | } 46 | }` 47 | 48 | try { 49 | const campaigns = await client.fetch(query) 50 | return campaigns 51 | } catch (error) { 52 | toast.error((error as {message: string}).message) 53 | return [] 54 | } 55 | } 56 | 57 | export const fetchApprovedCampaigns = async () => { 58 | const query = `*[_type == "campaign" && status == "approved"]{ 59 | _id, 60 | title, 61 | country, 62 | city, 63 | category, 64 | description, 65 | goalAmount, 66 | startDate, 67 | endDate, 68 | raiseMoneyFor, 69 | importance, 70 | impact, 71 | subAccountId, 72 | images, 73 | supportingDocuments, 74 | status, 75 | createdBy->{ 76 | _id, 77 | firstname, 78 | lastname, 79 | email 80 | } 81 | }` 82 | 83 | try { 84 | const approvedCampaigns = await client.fetch(query) 85 | return approvedCampaigns 86 | } catch (error) { 87 | toast.error((error as {message: string}).message) 88 | return [] 89 | } 90 | } 91 | 92 | export const fetchPendingCampaigns = async () => { 93 | const query = `*[_type == "campaign" && status == "pending"]{ 94 | _id, 95 | title, 96 | country, 97 | city, 98 | category, 99 | description, 100 | goalAmount, 101 | startDate, 102 | endDate, 103 | raiseMoneyFor, 104 | importance, 105 | impact, 106 | images, 107 | supportingDocuments, 108 | status, 109 | createdBy->{ 110 | _id, 111 | name, 112 | email 113 | } 114 | }` 115 | 116 | try { 117 | const pendingCampaigns = await client.fetch(query) 118 | return pendingCampaigns 119 | } catch (error) { 120 | toast.error((error as {message: string}).message) 121 | return [] 122 | } 123 | } 124 | 125 | export const fetchRejectedCampaigns = async () => { 126 | const query = `*[_type == "campaign" && status == "rejected"]{ 127 | _id, 128 | title, 129 | country, 130 | city, 131 | category, 132 | description, 133 | goalAmount, 134 | startDate, 135 | endDate, 136 | raiseMoneyFor, 137 | importance, 138 | impact, 139 | status, 140 | images, 141 | supportingDocuments, 142 | createdBy->{ 143 | _id, 144 | name, 145 | email 146 | } 147 | }` 148 | 149 | try { 150 | const rejectedCampaigns = await client.fetch(query) 151 | return rejectedCampaigns 152 | } catch (error) { 153 | toast.error((error as {message: string}).message) 154 | return [] 155 | } 156 | } 157 | 158 | export const fetchCampaignById = async (id: string | undefined) => { 159 | const query = `*[_type == "campaign" && _id == "${id}"]{ 160 | _id, 161 | title, 162 | country, 163 | city, 164 | category, 165 | description, 166 | goalAmount, 167 | startDate, 168 | endDate, 169 | raiseMoneyFor, 170 | importance, 171 | impact, 172 | status, 173 | name, 174 | phone, 175 | images, 176 | supportingDocuments, 177 | subAccountId, 178 | createdBy->{ 179 | _id, 180 | email 181 | } 182 | }` 183 | 184 | try { 185 | const campaign = await client.fetch(query) 186 | return campaign[0] // Return the first (and only) result 187 | } catch (error) { 188 | toast.error((error as {message: string}).message) 189 | return null 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /supporthive/schemaTypes/campaign.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'campaign', 3 | title: 'Campaign', 4 | type: 'document', 5 | fields: [ 6 | { 7 | name: 'createdBy', 8 | title: 'CreatedBy', 9 | type: 'reference', 10 | to: [{type: 'user'}], 11 | }, 12 | { 13 | name: 'title', 14 | title: 'Campaign Title', 15 | type: 'string', 16 | }, 17 | { 18 | name: 'country', 19 | title: 'Campaign Country', 20 | type: 'string', 21 | }, 22 | { 23 | name: 'city', 24 | title: 'Campaign City', 25 | type: 'string', 26 | }, 27 | { 28 | name: 'category', 29 | title: 'Campaign Category', 30 | type: 'string', 31 | options: { 32 | list: [ 33 | {title: 'Education', value: 'Education'}, 34 | {title: 'Health', value: 'Health'}, 35 | {title: 'Emergency Assistance', value: 'Emergency Assistance'}, 36 | {title: 'Community Development', value: 'Community Development'}, 37 | {title: 'Career', value: 'Career'}, 38 | {title: 'Other', value: 'Other'}, 39 | ], 40 | layout: 'radio', 41 | }, 42 | }, 43 | { 44 | name: 'description', 45 | title: 'Campaign Description', 46 | type: 'text', 47 | }, 48 | { 49 | name: 'goalAmount', 50 | title: 'Campaign Goal Amount', 51 | type: 'string', 52 | }, 53 | { 54 | name: 'startDate', 55 | title: 'Campaign Start Date', 56 | type: 'datetime', 57 | }, 58 | { 59 | name: 'endDate', 60 | title: 'Campaign End Date', 61 | type: 'datetime', 62 | }, 63 | { 64 | name: 'raiseMoneyFor', 65 | title: 'What do you want to raise money for?', 66 | type: 'text', 67 | }, 68 | { 69 | name: 'importance', 70 | title: 'Why is this campaign important to you?', 71 | type: 'text', 72 | }, 73 | { 74 | name: 'impact', 75 | title: 'What impact will this campaign have?', 76 | type: 'text', 77 | }, 78 | { 79 | name: 'images', 80 | title: 'Have Images related to your Campaign?', 81 | type: 'array', 82 | of: [{type: 'image'}], 83 | }, 84 | { 85 | name: 'supportingDocuments', 86 | title: 'Supporting Documents', 87 | type: 'array', 88 | of: [{type: 'file'}], 89 | }, 90 | { 91 | name: 'name', 92 | title: 'Name', 93 | type: 'string', 94 | }, 95 | { 96 | name: 'email', 97 | title: 'Email Address', 98 | type: 'string', 99 | }, 100 | { 101 | name: 'phone', 102 | title: 'Phone Number', 103 | type: 'string', 104 | }, 105 | { 106 | name: 'bank', 107 | title: 'Bank code', 108 | type: 'string', 109 | }, 110 | { 111 | name: 'accountNumber', 112 | title: 'Account Number', 113 | type: 'string', 114 | }, 115 | { 116 | name: 'subAccountId', 117 | title: 'subAccount Id', 118 | type: 'string', 119 | }, 120 | { 121 | name: 'status', 122 | title: 'Campaign Status', 123 | type: 'string', 124 | options: { 125 | list: [ 126 | {title: 'Pending', value: 'pending'}, 127 | {title: 'Approved', value: 'approved'}, 128 | {title: 'Rejected', value: 'rejected'}, 129 | ], 130 | }, 131 | initialValue: 'pending', 132 | }, 133 | ], 134 | } 135 | -------------------------------------------------------------------------------- /supporthive/schemaTypes/index.ts: -------------------------------------------------------------------------------- 1 | import campaign from './campaign' 2 | import user from './user' 3 | 4 | export const schemaTypes = [user, campaign] 5 | -------------------------------------------------------------------------------- /supporthive/schemaTypes/user.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'user', 3 | title: 'User', 4 | type: 'document', 5 | fields: [ 6 | { 7 | name: 'uid', 8 | title: 'User ID', 9 | type: 'string', 10 | }, 11 | { 12 | name: 'firstname', 13 | title: 'First Name', 14 | type: 'string', 15 | }, 16 | { 17 | name: 'lastname', 18 | title: 'Last Name', 19 | type: 'string', 20 | }, 21 | { 22 | name: 'email', 23 | title: 'Email', 24 | type: 'string', 25 | }, 26 | { 27 | name: 'emailVerified', 28 | title: 'Email Verified', 29 | type: 'boolean', 30 | }, 31 | { 32 | name: 'terms', 33 | title: 'Terms and Conditions Accepted', 34 | type: 'boolean', 35 | }, 36 | ], 37 | } 38 | -------------------------------------------------------------------------------- /supporthive/static/.gitkeep: -------------------------------------------------------------------------------- 1 | Files placed here will be served by the Sanity server under the `/static`-prefix 2 | -------------------------------------------------------------------------------- /supporthive/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "module": "Preserve", 10 | "moduleDetection": "force", 11 | "isolatedModules": true, 12 | "jsx": "preserve", 13 | "incremental": true 14 | }, 15 | "include": ["**/*.ts", "**/*.tsx"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | white: "#FFFFFF", 8 | black: "#333333", 9 | "gray-50": "#CCCCCC", 10 | "gray-100": "#D0D5DD", 11 | "Light-50": "#EAF6EC", 12 | "Light-100": "#DFF2E3", 13 | "Light-200": "#97D699", 14 | "normal-300": "#28A745", 15 | "normal-400": "#24963E", 16 | "normal-500": "#208637", 17 | "Dark-600": "#1E7D34", 18 | "Dark-700": "#186429", 19 | "Dark-800": "#124B1F", 20 | "Dark-900": "#000E04", 21 | "light-red": "#F62C23", 22 | "dark-red": "#F62C23", 23 | }, 24 | }, 25 | }, 26 | plugins: [], 27 | }; 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | // "include": ["src", "src/pages/Discover/DiscoverPage.tsx"], 24 | "include": ["src", "custom.d.ts", "vite-env.d.ts"], 25 | "references": [{ "path": "./tsconfig.node.json" }] 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] 3 | } 4 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from "vite"; 2 | 3 | import react from "@vitejs/plugin-react"; 4 | 5 | export default defineConfig(({ mode }) => { 6 | const env = loadEnv(mode, process.cwd(), ""); 7 | return { 8 | define: { 9 | "process.env": env, 10 | }, 11 | plugins: [react()], 12 | server: { 13 | host: "0.0.0.0", 14 | port: 5000, 15 | open: true, 16 | origin: "http://127.0.0.1:5000", 17 | }, 18 | preview: { 19 | host: "0.0.0.0", 20 | port: 3333, 21 | }, 22 | }; 23 | }); 24 | --------------------------------------------------------------------------------