51 Commits

Author SHA1 Message Date
CHIEFSOFT\ameye 10acd16efe fix vitals 2025-10-28 16:24:38 -04:00
CHIEFSOFT\ameye 1dc8e25cc8 REACT_APP_MAIN_API='http://simbrellang.net:6335' 2025-05-18 12:03:07 -04:00
CHIEFSOFT\ameye 9e25e73923 Added vat 2025-05-01 20:54:30 -04:00
victorAnumudu 9b0ad3c886 updated confirmation page 2025-03-10 12:12:16 +01:00
CHIEFSOFT\ameye b72fd8e939 fix txt 2025-03-05 16:36:28 -05:00
victorAnumudu 944fd5c4c1 bug fix 2025-03-04 19:58:34 +01:00
victorAnumudu 6d7db4d733 api issue fix 2025-03-04 19:52:51 +01:00
victorAnumudu eb340cfc1b api issue fix 2025-03-04 19:46:42 +01:00
victorAnumudu 61b7a62e1f bug fix 2025-03-04 19:15:09 +01:00
victorAnumudu eab8745468 loan repay API added 2025-03-04 19:12:19 +01:00
victorAnumudu 0064f5e4c3 loan info API added 2025-03-04 15:49:43 +01:00
victorAnumudu 9d7cb31094 not eligible screen bug fixed 2025-03-04 15:14:41 +01:00
victorAnumudu df7ca95653 missed screen added 2025-03-03 19:42:25 +01:00
victorAnumudu 348532d7cf pay loan dumy screens added 2025-03-03 13:11:06 +01:00
victorAnumudu 979d6d5c9e loan info dumy screen added 2025-03-03 12:20:36 +01:00
victorAnumudu e1b2eec88d fix checks issue 2025-03-02 23:13:13 +01:00
victorAnumudu d263a4e3df decimal issue fixed 2025-02-25 17:43:55 +01:00
victorAnumudu f1985ae180 decimal issue fixed 2025-02-25 17:34:04 +01:00
CHIEFSOFT\ameye f74e0ef436 Salary :oan ID 2025-02-25 11:24:59 -05:00
victorAnumudu 20231b65c0 text updated 2025-02-25 16:43:39 +01:00
victorAnumudu f360056f03 loan details screen added 2025-02-25 14:16:30 +01:00
victorAnumudu ab1192a508 terms and conditions added 2025-02-25 07:32:28 +01:00
victorAnumudu fcd72e401a terms and conditions added 2025-02-25 07:29:23 +01:00
victorAnumudu 8b45dd409f third screen hidden 2025-02-23 20:54:45 +01:00
victorAnumudu 4bf728a38d added verifyloan API 2025-02-18 00:12:11 +01:00
victorAnumudu dc2c1e7eab loan apply API added 2025-02-17 20:36:35 +01:00
victorAnumudu 9d86a67d98 loan select API added 2025-02-17 18:44:48 +01:00
victorAnumudu c96fc2710c login btn style adjusted 2025-02-12 10:46:41 +01:00
victorAnumudu baa0920be6 input tag style adjust 2025-02-12 10:40:41 +01:00
victorAnumudu bb26537920 App title changed 2025-02-12 09:48:16 +01:00
victorAnumudu 2e3902019b logout btn added 2025-02-12 09:41:33 +01:00
victorAnumudu b844a402c9 favicon added 2025-02-11 20:20:10 +01:00
victorAnumudu 29c33aca62 added verify pin API 2025-02-11 12:34:40 +01:00
victorAnumudu fb8d598d43 back to home link added 2025-02-10 19:53:56 +01:00
victorAnumudu b248cce3a5 adjusted offer screen 2025-02-10 16:03:54 +01:00
victorAnumudu 5533797d24 text change 2025-02-10 08:57:36 +01:00
victorAnumudu ab7657d07f get loan more screens added 2025-02-10 00:45:03 +01:00
victorAnumudu ec210390b3 changed get loan to pay loan 2025-02-06 20:58:10 +01:00
victorAnumudu d148cc24f4 next page screen added 2025-02-06 20:54:55 +01:00
victorAnumudu f7530e17eb get loan page products added 2025-02-06 19:27:40 +01:00
victorAnumudu 411d44d4e8 back to home page btn added 2025-02-06 08:49:32 +01:00
victorAnumudu 15a598872e login endpoint added 2025-02-05 21:23:45 +01:00
victorAnumudu b185419ba7 demo user list API integration started 2025-02-05 19:24:35 +01:00
CHIEFSOFT\ameye 44656bdfde updated URL 2025-02-05 13:15:53 -05:00
victorAnumudu e0d51a2ce3 bg added to get started page 2025-02-03 17:47:49 +01:00
victor.ebuka 60dd820f6e Merge branch 'axios-package-added' of DigiFi/digifi-SalaryLoan into master 2025-02-03 08:21:17 +00:00
victorAnumudu b827c69415 added axios package 2025-02-03 09:20:23 +01:00
victorAnumudu 114df2eca8 authorization component added 2025-02-03 08:57:32 +01:00
victorAnumudu 53090ed96f card hover style added 2025-02-03 08:25:31 +01:00
victorAnumudu 6d71eb13c1 hover transition added 2025-02-03 08:22:01 +01:00
victor.ebuka c9f80c5f41 Merge branch 'login-page' of DigiFi/digifi-SalaryLoan into master 2025-02-03 07:15:26 +00:00
44 changed files with 1517 additions and 117 deletions
+2 -1
View File
@@ -6,7 +6,8 @@ TWITTER_URL=https://twitter.com
INSTAGRAM_URL=https://www.instagram.com
# BACKEND END POINTS
VITE_USERS_ENDPOINT='https://digifi-apidev.chiefsoft.net/digiusers/v1'
REACT_APP_MAIN_API0='https://devcore.digifi.chiefsoft.net'
REACT_APP_MAIN_API='http://simbrellang.net:6335'
# ENQUIRIES CONTACTS
VITE_CALL_ENDPOINT='09099000000'
+2 -1
View File
@@ -6,7 +6,8 @@ VITE_TWITTER_URL=https://twitter.com
VITE_INSTAGRAM_URL=https://www.instagram.com
# BACKEND END POINTS
VITE_USERS_ENDPOINT='https://digifi-apidev.chiefsoft.net/digiusers/v1'
REACT_APP_MAIN_API0='https://devcore.digifi.chiefsoft.net'
REACT_APP_MAIN_API='http://simbrellang.net:6335'
# ENQUIRIES CONTACTS
VITE_CALL_ENDPOINT='09099000000'
+2 -1
View File
@@ -6,7 +6,8 @@ TWITTER_URL=https://twitter.com
INSTAGRAM_URL=https://www.instagram.com
# BACKEND END POINTS
VITE_USERS_ENDPOINT='https://digifi-apidev.chiefsoft.net/digiusers/v1'
REACT_APP_MAIN_API0='https://devcore.digifi.chiefsoft.net'
REACT_APP_MAIN_API='http://simbrellang.net:6335'
# ENQUIRIES CONTACTS
VITE_CALL_ENDPOINT='09099000000'
+2 -1
View File
@@ -14,7 +14,8 @@ services:
- "5173"
extra_hosts:
- digifi-apidev.chiefsoft.net:10.10.33.15
- backend.wrenchboard.api.test:10.10.33.15
- devcore.digifi.chiefsoft.net:10.10.33.15
- core.digifi.chiefsoft.net:10.10.33.15
environment:
- PORT=${DIGIFI_PORT}
tty: true
+8 -1
View File
@@ -3,12 +3,19 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@reduxjs/toolkit": "^2.5.1",
"@tanstack/react-query": "^5.66.0",
"axios": "^1.7.9",
"cra-template": "1.2.0",
"formik": "^2.4.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.4.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.1.5",
"react-scripts": "5.0.1"
"react-scripts": "5.0.1",
"redux": "^5.0.1",
"yup": "^1.6.1"
},
"scripts": {
"start": "react-scripts start",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+2 -2
View File
@@ -7,7 +7,7 @@
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
content="digiFi global back office systems"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
@@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>digiFi</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
+4 -20
View File
@@ -2,32 +2,12 @@
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
/*@media (prefers-reduced-motion: no-preference) {*/
/* .App-logo {*/
/* animation: App-logo-spin infinite 20s linear;*/
/* }*/
/*}*/
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
@@ -36,3 +16,7 @@
transform: rotate(360deg);
}
}
button, a {
cursor: pointer;
}
+16
View File
@@ -1,10 +1,26 @@
import { QueryClientProvider, QueryClient } from '@tanstack/react-query'
import MyRoutes from './MyRoutes';
import './App.css';
function App() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: 3,
// refetchOnMount: false,
staleTime: Infinity // can also be a number in millisecond
},
},
})
return (
<QueryClientProvider client={queryClient}>
<MyRoutes />
</QueryClientProvider>
)
}
+10 -1
View File
@@ -1,17 +1,26 @@
import React from 'react'
import {Routes, Route} from 'react-router-dom'
import UserExists from './authorization/UserExists'
import GetStartedPage from './pages/GetStartedPage'
import LoginPage from './pages/LoginPage'
import HomePage from './pages/HomePage'
import myLinks from './myLinks'
import GetLoanPage from './pages/GetLoanPage'
export default function MyRoutes() {
return (
<Routes>
<Route path={myLinks.getStarted} element={<GetStartedPage />} />
<Route path={myLinks.login} element={<LoginPage />} />
<Route path={myLinks.home} element={<HomePage />} />
<Route element={<UserExists />}>
<Route path={myLinks.home} element={<HomePage />} />
<Route path={myLinks.getLoan} element={<GetLoanPage />} />
</Route>
<Route path={myLinks.error} element={<LoginPage />} />
</Routes>
)
}
+37
View File
@@ -0,0 +1,37 @@
import React, { useEffect, useState } from 'react'
import { useLocation, useNavigate, Outlet } from 'react-router-dom'
import Layout from '../components/layout/Layout'
import myLinks from '../myLinks'
import PageLoader from '../components/PageLoader'
export default function UserExists() {
const {state} = useLocation()
const navigate = useNavigate()
const [loading, setLoading] = useState(true)
useEffect(()=>{
if(!state || localStorage.getItem('active') != 'true'){
return navigate(myLinks.getStarted, {replace:true})
}
setTimeout(()=>{
setLoading(false)
},2000)
},[])
return (
<>
{
loading ?
<PageLoader />
:
<Layout>
<Outlet />
</Layout>
}
</>
)
}
+104 -45
View File
@@ -1,49 +1,94 @@
// import React from 'react'
import { useQuery } from "@tanstack/react-query";
import { useNavigate } from 'react-router-dom'
import { CiPhone } from "react-icons/ci"
import { IoIosPhonePortrait } from "react-icons/io"
import { MdOutlineEmail } from "react-icons/md"
import { BsCash } from "react-icons/bs";
import { MdUpdate } from "react-icons/md"
import { FaUser, FaMapPin } from "react-icons/fa";
import { demoUsersList } from "../services/siteServices"
import queryKeys from "../services/queryKeys"
import myLinks from "../myLinks";
import TableWrapper from "./TableWrapper";
import formatNumber from "../helpers/formatNumber";
export default function HomeCom() {
const navigate = useNavigate()
const {data:users, isFetching, isError, error} = useQuery({
queryKey: queryKeys.demoUsers,
queryFn: () => demoUsersList()
})
const demoUsers = users?.data?.demo_data?.list // LOAN USERS LIST
const getLoanPage = (user) => {
navigate(myLinks.getLoan, {state:{user}})
}
return (
<div className="w-full h-screen flex flex-col gap-2 overflow-y-auto text-black bg-slate-100 p-5 sm:p-[40px]">
<div className="py-4 text-3xl text-black font-bold">Users</div>
<div className="grid gap-5 sm:gap-[40px] grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{data.map((item, index) => {
let color = item.place == 'Friends' ? 'text-emerald-500 bg-emerald-100/90' : item.place == 'Office' ? 'text-blue-500 bg-blue-100/90' : 'text-orange-500 bg-orange-100/90'
return (
<div key={index} className="w-full p-3 sm:p-5 bg-white shadow flex flex-col gap-5">
<div className="mb-5 card-title w-[70%] mx-auto">
<h1 className="mb-[1px] text-2xl font-bold">{item.name}</h1>
<span className={` ${color} text-sm font-bold p-1 rounded`}>{item.place}</span>
<div className="w-full flex flex-col gap-2 text-black">
<div className="py-3 text-3xl text-black font-bold">Users</div>
{isFetching ?
<>
<div className="w-full py-4">
<p className='text-slate-800'>Loading...</p>
</div>
</>
: isError ?
<div className="w-full py-4">
<p className='text-red-500'>{error.message}</p>
</div>
<div className="card-body flex flex-col gap-2">
<div className="contact text-slate-400 flex gap-3 items-center">
<span className="w-[40px] h-[40px] rounded-full flex flex-col justify-center items-center bg-slate-100/90 text-slate-400">
<IoIosPhonePortrait />
</span>
<span>{item.contact}</span>
</div>
<div className="contact text-slate-400 flex gap-3 items-center">
<span className="w-[40px] h-[40px] rounded-full flex flex-col justify-center items-center bg-slate-100/90 text-slate-400">
<CiPhone />
</span>
<span>{item.contact}</span>
</div>
<div className="contact text-slate-400 flex gap-3 items-center">
<span className="w-[40px] h-[40px] rounded-full flex flex-col justify-center items-center bg-slate-100/90 text-slate-400">
<MdOutlineEmail />
</span>
<span>{item.email}</span>
</div>
</div>
</div>
)
})}
:
<TableWrapper data={demoUsers} itemsPerPage={16}>
{({ data }) => (
<div className="grid gap-5 sm:gap-[40px] grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{data.map((user, index) => {
let hasSalaryAcct = user.salary_account === 0 ? false : true
return (
<div onClick={()=>getLoanPage(user)} key={user?.uid || index} className={`${hasSalaryAcct ? 'bg-white' : 'bg-red-50'} w-full rounded p-3 sm:p-5 shadow flex flex-col gap-5 hover:scale-105 hover:cursor-pointer`}>
<div className="mb-5 card-title w-full md:w-4/5 mx-auto text-center">
<h1 className="mb-[1px] text-base md:text-xl lg:text-2xl font-bold">{user.name}</h1>
<span className={`text-sm font-bold p-1 rounded`}>ID: {user.bvn}</span>
</div>
<div className="card-body flex flex-col gap-2">
<div className="contact text-slate-700 flex gap-2 items-center">
<span className="min-w-[30px] min-h-[30px] max-w-[30px] max-h-[30px] rounded-full flex flex-col justify-center items-center bg-slate-100/90 text-slate-700">
<FaUser />
</span>
<span>{user.customerid}</span>
</div>
<div className="contact text-slate-700 flex gap-3 items-center">
<span className="min-w-[30px] min-h-[30px] max-w-[30px] max-h-[30px] rounded-full flex flex-col justify-center items-center bg-slate-100/90 text-slate-700">
<BsCash />
</span>
<span>{formatNumber(user.balance)} Naira</span>
</div>
<div className="contact text-slate-700 flex gap-3 items-center">
<span className="min-w-[30px] min-h-[30px] max-w-[30px] max-h-[30px] rounded-full flex flex-col justify-center items-center bg-slate-100/90 text-slate-700">
<FaMapPin />
</span>
<span className="break-words">{user.pin}</span>
</div>
<div className="contact text-slate-700 flex gap-3 items-center">
<span className="min-w-[30px] min-h-[30px] max-w-[30px] max-h-[30px] rounded-full flex flex-col justify-center items-center bg-slate-100/90 text-slate-700">
<MdUpdate />
</span>
<span>{user.updated}</span>
</div>
</div>
</div>
)
})}
</div>
)}
</TableWrapper>
}
</div>
</div>
)
}
@@ -51,12 +96,26 @@ export default function HomeCom() {
const data = [
{name:'Jerry Eze', place: 'Home', contact: '021-025-0325', email: 'jerry@example.com'},
{name:'Mark John', place: 'Office', contact: '011-025-0311', email: 'mark@example.com'},
{name:'Larry Bon', place: 'Friends', contact: '033-025-0333', email: 'larry@example.com'},
{name:'Jeff Henry', place: 'Home', contact: '044-025-0344', email: 'jeff@example.com'},
{name:'Rose Ordor', place: 'Office', contact: '055-025-0355', email: 'rose@example.com'},
{name:'Mike Timothy', place: 'Friends', contact: '066-025-0366', email: 'mike@example.com'},
// const data = [
// {name:'Jerry Eze', place: 'Home', contact: '021-025-0325', email: 'jerry@example.com'},
// {name:'Mark John', place: 'Office', contact: '011-025-0311', email: 'mark@example.com'},
// {name:'Larry Bon', place: 'Friends', contact: '033-025-0333', email: 'larry@example.com'},
// {name:'Jeff Henry', place: 'Home', contact: '044-025-0344', email: 'jeff@example.com'},
// {name:'Rose Ordor', place: 'Office', contact: '055-025-0355', email: 'rose@example.com'},
// {name:'Mike Timothy', place: 'Friends', contact: '066-025-0366', email: 'mike@example.com'},
]
// {name:'Jerry Eze', place: 'Home', contact: '021-025-0325', email: 'jerry@example.com'},
// {name:'Mark John', place: 'Office', contact: '011-025-0311', email: 'mark@example.com'},
// {name:'Larry Bon', place: 'Friends', contact: '033-025-0333', email: 'larry@example.com'},
// {name:'Jeff Henry', place: 'Home', contact: '044-025-0344', email: 'jeff@example.com'},
// {name:'Rose Ordor', place: 'Office', contact: '055-025-0355', email: 'rose@example.com'},
// {name:'Mike Timothy', place: 'Friends', contact: '066-025-0366', email: 'mike@example.com'},
// {name:'Jerry Eze', place: 'Home', contact: '021-025-0325', email: 'jerry@example.com'},
// {name:'Mark John', place: 'Office', contact: '011-025-0311', email: 'mark@example.com'},
// {name:'Larry Bon', place: 'Friends', contact: '033-025-0333', email: 'larry@example.com'},
// {name:'Jeff Henry', place: 'Home', contact: '044-025-0344', email: 'jeff@example.com'},
// {name:'Rose Ordor', place: 'Office', contact: '055-025-0355', email: 'rose@example.com'},
// {name:'Mike Timothy', place: 'Friends', contact: '066-025-0366', email: 'mike@example.com'},
// ]
+2 -2
View File
@@ -1,9 +1,9 @@
import React from 'react'
export default function InputText({id, name, type='text'}) {
export default function InputText({id, name, type='text', value, handleChange}) {
return (
<div className='w-full h-12 round overflow-hidden'>
<input id={id} name={name} type={type} className='p-2 w-full h-full text-black outline-0 ring-0 border border-black rounded' />
<input id={id} name={name} type={type} value={value} onChange={handleChange} className='p-2 w-full h-full text-black outline-0 ring-0 border border-slate-400 focus:border-purple-600 rounded' />
</div>
)
}
+2 -2
View File
@@ -1,7 +1,7 @@
import React from 'react'
export default function Label({name, htmlfor}) {
export default function Label({name, htmlfor, error}) {
return (
<label className='text-black' htmlFor={htmlfor}>{name}</label>
<label className='text-black flex gap-1 items-center' htmlFor={htmlfor}>{name} {error && <span className='text-red-500 text-sm'>{error}</span>}</label>
)
}
+114
View File
@@ -0,0 +1,114 @@
import React, { useEffect, useState } from 'react'
import { useLocation, useNavigate, Link } from 'react-router-dom'
import { IoIosArrowBack } from "react-icons/io";
import myLinks from '../myLinks'
import PayloanScreens from './loan_screen/PayloanScreens';
import GetLoanScreens from './loan_screen/GetLoanScreens';
import LoanInfoScreens from './loan_screen/LoanInfoScreens';
export default function LoanScreens() {
const {state} = useLocation()
const navigate = useNavigate()
const [step, setStep] = useState({
details: {},
screen: [],
activeUser: state.user
})
const typeToShow = {
getloan: 'getloan',
payloan: 'payloan',
loaninfo: 'loaninfo',
}
const [showtype, setShowType] = useState(typeToShow.getloan)
const handleStep = (detailsToAdd, screen) => {
setStep(prev => ({...prev, details: {...prev.details, ...detailsToAdd}, screen: [...prev.screen, screen]}))
}
const handleBackBtn = () => {
// if(step?.screen?.length <= 0 || step?.screen[step.screen.length -1 ] == screens.finished){
// setStep({details: {}, screen: []})
// return navigate(myLinks.home, {state:{proceed:'true'}})
// }
if(step?.screen[step.screen.length -1 ] == screens.finished){
setStep({details: {}, screen: []})
return navigate(myLinks.home, {state:{proceed:'true'}})
}
setStep(prev => ({...prev, screen: prev.screen.slice(0, -1)}))
}
let screens = showtype == typeToShow.getloan ? {
products: 'products-screen',
terms_conditions: 'terms_conditions',
getLoan: 'get-loan',
pin: 'pin-screen',
loan_details: 'loan_details',
offers: 'offers-screen',
selected_offer: 'selected-offer-screen',
loan_pin: 'loan-pin-screen',
not_eligible: 'not-eligible',
finished: 'finished'
} : showtype == typeToShow.payloan ? {
loan_list: 'loan_list',
repay_pin: 'repay_pin',
finished: 'finished'
}: showtype == typeToShow.loaninfo ? {
loan_info: 'loan_info',
finished: 'finished'
}: null
useEffect(()=>{
if(!state?.user){
return navigate(myLinks.getStarted, {replace:true})
}
},[])
useEffect(()=>{ // reset set details whenever the type of screen route to display changes
setStep({ details: {}, screen: [], activeUser: state.user})
},[showtype])
return (
<div className={`flex flex-col items-center justify-center`}>
<div className='relative flex flex-col gap-4 w-[80%] sm:w-[400px] h-[600px] bg-white rounded-xl p-4 sm:p-8 shadow'>
<div className="relative pb-3 card-title w-full border-b-2 flex gap-4 items-center">
<div className={` ${!step.screen.length && 'hidden'} absolute left-2 top-1/2 -translate-y-1/2 p-1 cursor-pointer`} onClick={handleBackBtn}>
<IoIosArrowBack className='text-3xl sm:text-5xl font-bold text-orange-500 cursor-pointer' />
</div>
<div className='w-full text-center'>
<h1 className="mb-[1px] text-2xl font-bold">{state?.user.name}</h1>
<span className={`text-base font-bold p-1 rounded`}>ID: {state?.user.bvn}</span>
</div>
</div>
{showtype == typeToShow.getloan &&
<>
<GetLoanScreens step={step} screens={screens} typeToShow={typeToShow} setShowType={setShowType} handleStep={handleStep} />
</>
}
{showtype == typeToShow.payloan &&
<>
<PayloanScreens step={step} screens={screens} handleStep={handleStep} />
</>
}
{showtype == typeToShow.loaninfo &&
<>
<LoanInfoScreens step={step} screens={screens} handleStep={handleStep} />
</>
}
<div className='absolute left-0 bottom-1 w-full flex justify-center items-center'>
<Link to={myLinks.home} className='font-semibold text-center border-b border-transparent hover:border-purple-400'>Back to <span className='text-purple-400 '>Home</span></Link>
</div>
</div>
</div>
)
}
+149
View File
@@ -0,0 +1,149 @@
import { useEffect, useState } from "react";
const data1 = [];
// type PaginatedListProps = {
// data: { name: string; email: string; status: string; location: string; }[],
// itemsPerPage: number,
// filterItem?: string[],
// titleClass?:string,
// children: (data:any) => ReactNode;
// }
export default function TableWrapper({
data = data1,
itemsPerPage = 5,
filterItem,
children,
}) {
const [isLoading, setIsLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState("");
const [filteredData, setFilteredData] = useState(data);
const [currentPage, setCurrentPage] = useState(0);
const [newData, setNewData] = useState([]);
const numberOfSelection = itemsPerPage;
const handlePrev = () => {
if (currentPage != 0) {
setCurrentPage((prev) => prev - numberOfSelection);
}
};
const handleNext = () => {
if (currentPage < data.length) {
setCurrentPage((prev) => prev + numberOfSelection);
}
};
const handleSearch = ({ target: { value } }, name) => {
setSearchTerm(value);
let newFilteredData = data.filter((item) =>
item[name].toLowerCase().startsWith(value.toLowerCase())
);
setFilteredData(newFilteredData);
setCurrentPage(0);
};
useEffect(() => {
setIsLoading(true)
setTimeout(()=>{
setNewData(
filteredData?.slice(currentPage, numberOfSelection + currentPage)
);
setIsLoading(false)
},1000)
}, [currentPage, filteredData]);
useEffect(()=>{
setCurrentPage(0)
},[itemsPerPage])
return (
<div className="py-4 w-full bg-transparent rounded-md">
{data.length > 0 && filterItem && (
<div className="mb-10 flex justify-end items-center gap-2">
{filterItem.map((item, index) => (
<label
key={index}
className="flex flex-col sm:flex-row items-center gap-2 text-slate-600 dark:text-slate-100 transition-all duration-500"
>
Search by {item[0].toUpperCase() + item.slice(1)}
<input
name={item}
type="text"
className="py-1 px-2 text-sm min-w-[100px] text-black dark:text-white bg-white dark:bg-slate-800 rounded-full border-0 outline-none ring-1 ring-slate-300 dark:ring-white transition-all duration-500"
value={searchTerm}
onChange={(e) => {
handleSearch(e, item);
}}
/>
</label>
))}
</div>
)}
<div className="flex flex-col">
<div className="w-full">
{children({ data: newData })}
</div>
{/* PAGINATION BUTTON */}
<div className='mt-6 p-2 w-full flex flex-col lg:flex-row justify-center items-center gap-3 md:gap-6'>
<div className="text-sm text-center lg:text-left font-normal text-gray-500 dark:text-gray-400 block w-full">Showing <span className="font-semibold text-gray-900 dark:text-white">
{isLoading ? '----' : `${currentPage + 1}-${currentPage + numberOfSelection >= data.length ? data.length : (currentPage + numberOfSelection)}`}</span> of <span className="font-semibold text-gray-900 dark:text-white">{data.length}</span>
</div>
<div className='flex items-center gap-3 md:gap-6'>
<button
onClick={handlePrev}
className={`px-4 py-2 rounded ${currentPage == 0 ? 'bg-sky-600/50 pointer-events-none' : 'bg-sky-600'} text-white-light`}
disabled={isLoading}
>
Prev
</button>
<button
onClick={handleNext}
className={`px-4 py-2 rounded ${currentPage + numberOfSelection >= data.length ? 'bg-sky-600/50 pointer-events-none' : 'bg-sky-600'} text-white-light`}
disabled={isLoading}
>
Next
</button>
</div>
</div>
</div>
{/* show prev and next button if data exist */}
{/* {data.length > 0 && (
{data.length && data.map((item, index)=>{
item = item
if(index%itemsPerPage == 0 && index >= currentPage && index <= currentPage+itemsPerPage){
return (
<button
key={index}
onClick={handleNext}
className={`w-6 h-6 md:w-12 md:h-12 text-sm md:text-lg rounded-full flex justify-center items-center border transition-all duration-300 ${
currentPage != index
? "text-slate-400 border-slate-400 dark:text-slate-400 dark:border-slate-400"
: "text-slate-600 border-slate-600 dark:text-white dark:border-white pointer-events-none"
}`}
>
{index/itemsPerPage +1}
</button>
)
}
})}
</div>
)} */}
{isLoading && <TableIsLoading />}
</div>
);
}
const TableIsLoading = () => {
return (
<div className="w-full fixed z-[999] inset-0 bg-slate-600/30 flex justify-center items-center">
<p className="rounded-md shadow-md p-4 bg-white/80 dark:bg-gray-900 text-brown dark:text-white">Loading...</p>
</div>
)
}
+5 -5
View File
@@ -5,14 +5,14 @@ import myLinks from '../../myLinks';
export default function GetStarted() {
return (
<div className="App">
<header className="App-header">
<img src={logo2} className="App-logo" alt="logo" />
<p className=''>
<div className={`h-screen bg-sky-300 flex flex-col items-center justify-center bg-[url('./assets/first-background.jpg')] bg-cover bg-center bg-no-repeat`}>
<header className="flex flex-col gap-2 justify-center items-center text-lg sm:text-2xl font-bold">
<img src={logo2} className="h-32 sm:h-72 w-auto" alt="logo" />
<p className='w-full text-center'>
digiFi - Banking Offers Emulator Systems.
</p>
<Link
className="App-link"
className="mt-2 px-2 py-1 text-emerald-600 bg-emerald-100/90 rounded"
to={myLinks.login}
href="https://reactjs.org"
// target="_blank"
+49 -4
View File
@@ -1,15 +1,52 @@
import React, { useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useMutation } from '@tanstack/react-query'
import myLinks from '../../myLinks'
import Label from '../Label'
import InputText from '../InputText'
import PageLoader from '../PageLoader'
import { loginUser } from '../../services/siteServices'
export default function LoginCom() {
const [loading, setLoading] = useState(true)
const {state} = useLocation()
const navigate = useNavigate()
const [fields, setFields] = useState({
username: '',
password: '',
})
const handleChange = ({target:{name, value}}) => {
setFields(prev => ({...prev, [name]:value}))
}
const login = useMutation({
mutationFn: (fields) => {
if(!fields.username || !fields.password){
throw new Error('Please provide all fields marked *')
}
return loginUser(fields)
},
onError: (error) => {
console.log(error)
},
onSuccess: (res) => {
// const {token, room} = res?.data?.data
// if(token){
// localStorage.setItem('token', token)
// localStorage.setItem('room', room)
// // const data = {token}
// // dispatch(updateUserDetails({ ...data }));
// }
navigate(myLinks.home, {state:{proceed:'true'}}) // later add redux to dispatch state
localStorage.setItem('active', 'true')
}
})
useEffect(()=>{
if(state?.proceed != 'true'){
return navigate(myLinks.getStarted, {replace:true})
@@ -25,18 +62,26 @@ export default function LoginCom() {
<PageLoader />
:
<div className={`h-screen bg-sky-300 flex flex-col items-center justify-center bg-[url('./assets/first-background.jpg')] bg-cover bg-center bg-no-repeat`}>
<div className='flex flex-col gap-4 w-[80%] sm:w-[500px] bg-white rounded-xl p-3 sm:p-5 shadow'>
<div className='flex flex-col gap-4 w-[80%] sm:w-[500px] bg-white rounded p-5 sm:p-[30px] shadow'>
<div className='text-input'>
<Label name='Username' htmlfor='username' />
<InputText id='username' name='username' />
<InputText id='username' name='username' value={fields.username} handleChange={handleChange} />
</div>
<div className='text-input'>
<Label name='Password' htmlfor='password' />
<InputText id='password' name='password' type='password' />
<InputText id='password' name='password' type='password' value={fields.password} handleChange={handleChange} />
</div>
{login.error &&
<>
<div className="w-full text-center p-2">
<p className='text-red-500 text-sm'>{login.error.message}</p>
</div>
</>
}
<div className='mt-5 flex justify-end items-center'>
<button onClick={()=>navigate(myLinks.home, {state:{proceed:'true'}})} className='px-3 py-2 bg-purple-800 text-white font-bold rounded'>Login</button>
<button onClick={()=>{login.mutate(fields)}} disabled={login.isPending} className='px-4 py-2 bg-purple-800 text-white font-bold rounded'>{login.isPending ? 'loading...' : 'Login'}</button>
</div>
</div>
</div>
+23
View File
@@ -0,0 +1,23 @@
import {Outlet, useNavigate} from 'react-router-dom'
import myLinks from '../../myLinks'
export default function Layout() {
const navigate = useNavigate()
const handleLogout = () => {
navigate(myLinks.login, {state:{proceed:'false'}, replace:true})
localStorage.clear()
}
return (
<div className={`w-full h-screen overflow-y-auto flex flex-col gap-4 bg-[url('./assets/first-background.jpg')] bg-cover bg-center bg-no-repeat`}>
<div className='sticky top-0 w-full flex justify-end p-5 pb-0 sm:p-[30px] sm:pb-0'>
<button onClick={handleLogout} className='bg-white px-4 py-2 rounded text-red-500 font-bold text-xl'>Logout</button>
</div>
<div className='p-5 pb-0 sm:p-[30px] sm:pb-0'>
<Outlet />
</div>
</div>
)
}
@@ -0,0 +1,59 @@
import React from 'react'
import Pin from './getloan/Pin';
import Offers from './getloan/Offers';
import LoanPin from './getloan/LoanPin';
import SelectedOffer from './getloan/SelectedOffer';
import GetLoan from './getloan/GetLoan';
import TermsAndConditions from './getloan/TermsAndConditions';
import LoanDetails from './getloan/LoanDetails';
import ProductDetails from './getloan/ProductDetails';
import NotEligible from './getloan/NotEligible';
export default function GetLoanScreens({step, screens, typeToShow, setShowType, handleStep}) {
return (
<>
{!step?.screen?.length || step?.screen[step.screen.length -1 ] == screens.products ?
<ProductDetails step={step} handleStep={handleStep} screens={screens} />
: step?.screen[step.screen.length -1 ] == screens.getLoan ?
<>
<GetLoan step={step} handleStep={handleStep} screens={screens} typeToShow={typeToShow} setShowType={setShowType} />
</>
:step?.screen[step.screen.length -1 ] == screens.not_eligible?
<>
<NotEligible step={step} handleStep={handleStep} screens={screens} />
</>
:step?.screen[step.screen.length -1 ] == screens.terms_conditions ?
<>
<TermsAndConditions step={step} handleStep={handleStep} screens={screens} />
</>
:step?.screen[step.screen.length -1 ] == screens.pin ?
<>
<Pin step={step} handleStep={handleStep} screens={screens} />
</>
: step?.screen[step.screen.length -1 ] == screens.offers ?
<>
<Offers step={step} handleStep={handleStep} screens={screens} />
</>
: step?.screen[step.screen.length -1 ] == screens.selected_offer ?
<>
<SelectedOffer step={step} handleStep={handleStep} screens={screens} />
</>
: step?.screen[step.screen.length -1 ] == screens.loan_details?
<>
<LoanDetails step={step} handleStep={handleStep} screens={screens} />
</>
:step?.screen[step.screen.length -1 ] == screens.loan_pin ?
<>
<LoanPin step={step} handleStep={handleStep} screens={screens} />
</>
:step?.screen[step.screen.length -1 ] == screens.finished ?
<>
<LoanPin step={step} handleStep={handleStep} screens={screens} />
</>
: null
}
</>
)
}
@@ -0,0 +1,21 @@
import React from 'react'
import LoanInfoList from './loaninfo/LoanInfoList';
export default function LoanInfoScreens({step, screens, typeToShow, setShowType, handleStep}) {
return (
<>
{!step?.screen?.length || step?.screen[step.screen.length -1 ] == screens.loan_list ?
<LoanInfoList step={step} handleStep={handleStep} screens={screens} />
: step?.screen[step.screen.length -1 ] == screens.repay_pin ?
<>
{/* <GetLoan step={step} handleStep={handleStep} screens={screens} typeToShow={typeToShow} setShowType={setShowType} /> */}
{null}
</>
: null
}
</>
)
}
@@ -0,0 +1,26 @@
import React from 'react'
import RepayLoanList from './payloan/RepayLoanList';
import PinRepayment from './payloan/PinRepayment';
export default function PayloanScreens({step, screens, typeToShow, setShowType, handleStep}) {
return (
<>
{!step?.screen?.length || step?.screen[step.screen.length -1 ] == screens.loan_list ?
<RepayLoanList step={step} handleStep={handleStep} screens={screens} />
: step?.screen[step.screen.length -1 ] == screens.repay_pin ?
<>
{/* <GetLoan step={step} handleStep={handleStep} screens={screens} typeToShow={typeToShow} setShowType={setShowType} /> */}
<PinRepayment step={step} handleStep={handleStep} screens={screens} />
</>
: step?.screen[step.screen.length -1 ] == screens.finished ?
<>
<PinRepayment step={step} handleStep={handleStep} screens={screens} />
</>
:null
}
</>
)
}
@@ -0,0 +1,31 @@
import React from 'react'
export default function GetLoan({step, handleStep, screens, typeToShow, setShowType}) {
const handleGetLoan = () => {
// handleStep({...step.details}, screens.pin)
handleStep({...step.details}, screens.terms_conditions)
}
const handlePayLoan = () => {
setShowType(typeToShow.payloan)
}
const handleLoanInfo = () => {
setShowType(typeToShow.loaninfo)
}
return (
<div className='mt-3 h-full flex flex-col gap-3'>
<div className='mt-3 flex justify-center'>
<button onClick={handleGetLoan} className='p-3 bg-purple-800 text-lg sm:text-2xl text-white font-bold rounded'>Get Loan</button>
</div>
<div className='mt-3 w-full h-full flex flex-col gap-2 justify-end'>
<button onClick={handlePayLoan} className='w-full p-3 bg-orange-500 text-lg sm:text-2xl text-white font-bold rounded'>Pay Loan</button>
<button onClick={handleLoanInfo} className='w-full p-3 bg-orange-500 text-lg sm:text-2xl text-white font-bold rounded'>Loan Information</button>
</div>
</div>
)
}
@@ -0,0 +1,24 @@
import React from 'react'
export default function LoanDetails({step, handleStep, screens}) {
const handleGetLoan = () => {
handleStep({...step.details}, screens.loan_pin)
}
return (
<div className='mt-3 h-full flex flex-col gap-3'>
<p className='font-semibold text-base md:text-xl'>Loan Details</p>
<div className='text-input w-full flex flex-col gap-2 justify-center items-center'>
<p className='w-full text-base md:text-xl'>Loan Amount: {step.details.typedAmount}</p>
<p className='w-full text-base md:text-xl'>Interest 5%: {Math.round((step.details.typedAmount*0.05 + Number.EPSILON) * 100) / 100}</p>
<p className='w-full text-base md:text-xl'>Mgt Fee 1%: {Math.round((step.details.typedAmount*0.01 + Number.EPSILON) * 100) / 100}</p>
<p className='w-full text-base md:text-xl'>Insurance 1%: {Math.round((step.details.typedAmount*0.01 + Number.EPSILON) * 100) / 100}</p>
<p className='w-full text-base md:text-xl'>VAT(3.75% Int)%: {Math.round((0.0375 * step.details.typedAmount*0.05 + Number.EPSILON) * 100) / 100}</p>
</div>
<div className='mt-3 flex justify-center'>
<button onClick={handleGetLoan} className='p-3 bg-purple-800 text-lg sm:text-2xl text-white font-bold rounded'>Accept</button>
</div>
</div>
)
}
@@ -0,0 +1,73 @@
import React, { useState } from 'react'
import { useMutation } from '@tanstack/react-query'
import Label from '../../Label'
import InputText from '../../InputText'
import { verifyLoan } from '../../../services/siteServices'
export default function LoanPin({step, handleStep, screens}) {
const [isSuccess, setIsSuccess] = useState(false)
const [error, setError] = useState('')
const [pin, setPin] = useState('')
const handlePinChange = ({target:{value}}) => {
setError('')
setPin(value)
}
const proceed = useMutation({
mutationFn: () => {
let fields = {pin, bvn:step.activeUser.bvn, loan_application_id:step.details.loan_application_id}
if(!fields.pin){
throw ({message:'Please enter pin'})
}
// if(isNaN(fields.pin)){
// throw ({message:'Amount must be a valid figure'})
// }
if(fields.pin.length != 4){
throw ({message:'Pin must be 4 digits'})
}
return verifyLoan(fields)
},
onError: (error) => {
setError(error.message)
setTimeout(()=>{setError('')},3000)
},
onSuccess: (res) => {
setIsSuccess(true)
handleStep({...step}, screens.finished)
}
})
return (
<>
{!isSuccess ?
<div className='mt-3 flex flex-col gap-4'>
<div className='text-input font-semibold w-full flex flex-col gap-4 justify-center items-center'>
<p className='text-center text-base md:text-xl'>{`Your loan of ₦${step.details.typedAmount} would be set up. T&C's apply.`}</p>
<Label name='Enter your 4 Digit pin' htmlfor='pin' />
<InputText id='pin' name='pin' type='text' value={pin} handleChange={handlePinChange} />
<button onClick={()=>{proceed.mutate()}} disabled={proceed.isPending} className='p-3 bg-purple-800 text-base sm:text-xl text-white font-bold rounded'>{proceed.isPending ? 'loading...' : 'Continue'}</button>
</div>
</div>
:
<div className='mt-3 flex flex-col gap-4'>
<div className='text-input font-semibold w-full flex flex-col gap-4 justify-center items-center'>
<p className='text-center text-base md:text-xl'>Your request is being processed. You will receive a confirmation SMS shortly.</p>
</div>
</div>
}
{error &&
<>
<div className="w-full text-center p-2">
<p className='text-red-500 text-sm'>{error}</p>
</div>
</>
}
</>
)
}
@@ -0,0 +1,20 @@
import React from 'react'
export default function NotEligible({step, handleStep, screens}) {
// const handleGetLoan = () => {
// handleStep({...step.details}, screens.getloan)
// }
return (
<div className='mt-3 h-full flex flex-col gap-3'>
<p className='text-center text-base sm:text-lg text-red-500 font-medium'>Selected profile not qualified for this product</p>
{/* <div className='mt-3 w-full h-full flex flex-col gap-2 justify-end'>
<button className='w-full p-3 bg-orange-500 text-lg sm:text-2xl text-white font-bold rounded'>Pay Loan</button>
<button className='w-full p-3 bg-orange-500 text-lg sm:text-2xl text-white font-bold rounded'>Loan Information</button>
</div> */}
</div>
)
}
@@ -0,0 +1,89 @@
import React, { useState } from 'react'
import { useQuery } from "@tanstack/react-query";
import { useMutation } from '@tanstack/react-query'
import { IoIosArrowForward } from "react-icons/io";
import { getOffers, loanSelect } from '../../../services/siteServices'
import queryKeys from '../../../services/queryKeys'
export default function Offers({step, handleStep, screens}) {
const [selectedAmount, setSelectedAmount] = useState('')
const {data, isFetching, isError, error} = useQuery({
queryKey: queryKeys.offers,
queryFn: () => getOffers()
})
const offers = data?.data?.product_data?.offers // OFFERS LIST
const selectedLoan = useMutation({
mutationFn: (details) => {
let fields = {bvn:step.activeUser.bvn, loan: details.loan}
// if(!fields.bvn){
// throw new Error('*')
// }
setSelectedAmount(details.amount)
return loanSelect(fields)
},
onError: (error) => {
// setError('*')
},
onSuccess: (res) => {
const selectedOffer = res.data.loan
handleStep({selectedAmount, selectedOffer, ...res.data}, screens.selected_offer)
}
})
return (
<div className='mt-3 flex flex-col gap-4'>
<div className='text-input font-semibold w-full flex flex-col gap-4 justify-center items-center'>
{isFetching ?
<>
<div className="w-full py-4">
<p className='text-slate-800 text-center'>Loading...</p>
</div>
</>
: isError ?
<div className="w-full py-4">
<p className='text-red-500 text-center'>{error.message}</p>
</div>
:
<>
{offers.map(item => {
let isDisabled = item.active == '0' ? true : false
return (
<button
key={item?.cid}
disabled={isDisabled || selectedLoan.isPending}
// value={item.loan}
onClick={()=>selectedLoan.mutate(item)}
className={`w-full flex gap-2 justify-between items-center p-2 bg-purple-800 ${selectedLoan.isPending && 'bg-purple-800/50'} text-base sm:text-xl text-white font-bold rounded ${isDisabled && 'opacity-50'}`}
>
{item?.description}
<IoIosArrowForward />
</button>
)
})}
</>
}
<>
</>
{selectedLoan.isPending &&
<div className="w-full text-center p-2">
<p className='text-sm'>loading...</p>
</div>
}
{selectedLoan.error &&
<>
<div className="w-full text-center p-2">
<p className='text-red-500 text-sm'>{selectedLoan.error.message}</p>
</div>
</>
}
</div>
</div>
)
}
@@ -0,0 +1,54 @@
import React, { useState } from 'react'
import { useMutation } from '@tanstack/react-query'
import Label from '../../Label'
import InputText from '../../InputText'
import { verifyPin } from '../../../services/siteServices'
export default function Pin({step, handleStep, screens}) {
const [error, setError] = useState('')
const [pin, setPin] = useState('')
const handlePinChange = ({target:{value}}) => {
setError('')
setPin(value)
}
const proceed = useMutation({
mutationFn: () => {
let fields = {pin, bvn:step.activeUser.bvn}
if(!fields.pin){
throw new Error('*')
}
return verifyPin(fields)
},
onError: (error) => {
setError('*')
},
onSuccess: (res) => {
handleStep(step.details, screens.offers)
}
})
return (
<div className='mt-3 flex flex-col gap-4'>
{(!proceed.error || proceed?.error?.message == '*') &&
<div className='text-input font-semibold w-full flex flex-col gap-4 justify-center items-center'>
<Label name='Enter your pin' htmlfor='pin' error={error} />
<InputText id='pin' name='pin' type='text' value={pin} handleChange={handlePinChange} />
<button onClick={()=>{proceed.mutate()}} disabled={proceed.isPending} className='p-3 bg-purple-800 text-base sm:text-xl text-white font-bold rounded'>{proceed.isPending ? 'loading...' : 'Continue'}</button>
</div>
}
{(proceed.error && proceed.error.message != '*') &&
<>
<div className="w-full text-center p-2">
<p className='text-red-500 font-semibold text-base sm:text-3xl'>{proceed.error.message}</p>
</div>
</>
}
</div>
)
}
@@ -0,0 +1,59 @@
import React from 'react'
import { useQuery } from "@tanstack/react-query";
import queryKeys from '../../../services/queryKeys';
import { getProducts } from '../../../services/siteServices';
import { IoIosArrowForward } from "react-icons/io";
export default function ProductDetails({step, handleStep, screens}) {
const {data, isFetching, isError, error} = useQuery({
queryKey: queryKeys.products,
queryFn: () => getProducts()
})
const products = data?.data?.product_data?.products // PRODUCTS LIST
const handleClick = (product) => {
if(step?.activeUser?.salary_account){
handleStep(product, screens.getLoan)
}else{
handleStep({...step.details}, screens.not_eligible)
}
}
return (
<>
<div className='mt-3 flex flex-col gap-3'>
{isFetching ?
<>
<div className="w-full py-4">
<p className='text-slate-800 text-center'>Loading...</p>
</div>
</>
: isError ?
<div className="w-full py-4">
<p className='text-red-500 text-center'>{error.message}</p>
</div>
:
<>
{products && products.map(product => {
let isDisabled = product.active == '0' ? true : false
return (
<button
key={product?.cid}
disabled={isDisabled}
onClick={()=>handleClick(product)}
className={`w-full flex gap-2 justify-between items-center p-2 bg-purple-800 text-base sm:text-xl text-white font-bold rounded ${isDisabled && 'opacity-50'}`}
>
<span>{product?.description}</span>
<IoIosArrowForward />
</button>
)
}
)}
</>
}
</div>
</>
)
}
@@ -0,0 +1,68 @@
import React, { useState } from 'react'
import { useMutation } from '@tanstack/react-query'
import Label from '../../Label'
import InputText from '../../InputText'
import { loanApply } from '../../../services/siteServices'
export default function SelectedOffer({step, handleStep, screens}) {
const [amount, setAmount] = useState('')
const [error, setError] = useState('')
const handleAmountChange = ({target:{value}}) => {
setError('')
setAmount(value)
}
const apply = useMutation({
mutationFn: () => {
let fields = {amount: Number(amount), bvn:step.activeUser.bvn, loan:step.details.loan}
if(!fields.amount){
// throw new Error('*')
throw ({message:'Please enter amount'})
}
if(fields.amount > Number(step.details.selectedAmount)){
// throw new Error('*')
throw ({message:`Please enter amount not more than ${step.details.selectedAmount}`})
}
if(isNaN(amount)){
throw ({message:'Amount must be a valid figure'})
}
return loanApply(fields)
},
onError: (error) => {
setError(error.message)
setTimeout(()=>{setError('')},3000)
},
onSuccess: (res) => {
// const selectedOffer = res.data.loan[0].loan
handleStep({typedAmount: Number(amount),loan_application_id:res.data.loan_application_id[0], ...step.details}, screens.loan_details)
}
})
return (
<div className='mt-3 flex flex-col gap-4'>
<div className='text-input font-semibold w-full flex flex-col gap-4 justify-center items-center'>
<p className='text-center text-base'>Your eligible loan amount is {step.details.selectedAmount}</p>
<Label name='Please enter your desired loan amount' htmlfor='amount' />
<InputText id='amount' name='amount' type='text' value={amount} handleChange={handleAmountChange} />
{/* <div className='flex flex-col gap-4'>
<p className='text-center text-base'>Upfront Interest 200 Naira</p>
<p className='text-center text-base'>Due Date: Next Salary or 30 Days</p>
</div> */}
<button onClick={()=>{apply.mutate()}} disabled={apply.isPending} className='p-3 bg-purple-800 text-base sm:text-xl text-white font-bold rounded'>{apply.isPending ? 'loading...' : 'Continue'}</button>
</div>
{error &&
<>
<div className="w-full text-center p-2">
<p className='text-red-500 text-sm'>{error}</p>
</div>
</>
}
</div>
)
}
@@ -0,0 +1,19 @@
import React from 'react'
export default function TermsAndCondition({step, handleStep, screens}) {
const handleGetLoan = () => {
handleStep({...step.details}, screens.offers)
}
return (
<div className='mt-3 h-full flex flex-col gap-3'>
<div className='text-input font-semibold w-full flex flex-col gap-4 justify-center items-center'>
<p className='text-center text-base md:text-xl'>I accept the Terms and Conditions</p>
</div>
<div className='mt-3 flex justify-center'>
<button onClick={handleGetLoan} className='p-3 bg-purple-800 text-lg sm:text-2xl text-white font-bold rounded'>Accept</button>
</div>
</div>
)
}
@@ -0,0 +1,96 @@
import React, { useState } from 'react'
import { useQuery } from "@tanstack/react-query";
import { useMutation } from '@tanstack/react-query'
import { IoIosArrowForward } from "react-icons/io";
import { getLoanInfo, loanSelect } from '../../../services/siteServices'
import queryKeys from '../../../services/queryKeys'
export default function LoanInfoList({step, handleStep, screens}) {
const [selectedAmount, setSelectedAmount] = useState('')
const {data, isFetching, isError, error} = useQuery({
queryKey: queryKeys.offers,
queryFn: () => getLoanInfo()
})
const loanInfoList = data?.data // LOAN INFO LIST
const selectedLoan = useMutation({
mutationFn: (details) => {
let fields = {bvn:step.activeUser.bvn, loan: details.loan}
// if(!fields.bvn){
// throw new Error('*')
// }
setSelectedAmount(details.amount)
return loanSelect(fields)
},
onError: (error) => {
// setError('*')
},
onSuccess: (res) => {
const selectedOffer = res.data.loan
handleStep({selectedAmount, selectedOffer, ...res.data}, screens.repay_pin)
}
})
const handleClick = (selectedAmount) => { // remove later
handleStep({selectedAmount}, screens.repay_pin)
}
return (
<div className='mt-3 flex flex-col gap-4'>
<div className='text-input font-semibold w-full flex flex-col gap-4 justify-center items-center'>
{isFetching ?
<>
<div className="w-full py-4">
<p className='text-slate-800 text-center'>Loading...</p>
</div>
</>
: isError ?
<div className="w-full py-4">
<p className='text-red-500 text-center'>{error.message}</p>
</div>
:
<>
<p className='text-center text-base md:text-xl'>Loan to repay</p>
{loanInfoList.map(item => {
let isDisabled = item.active == '0' ? true : false
return (
<button
key={item?.cid}
disabled={true}
// disabled={isDisabled || selectedLoan.isPending}
// value={item.loan}
// onClick={()=>selectedLoan.mutate(item)}
onClick={()=>handleClick(item?.amount)}
className={`w-full flex gap-2 justify-between items-center p-2 bg-purple-800 ${selectedLoan.isPending && 'bg-purple-800/50'} text-base sm:text-xl text-white font-bold rounded ${isDisabled && 'opacity-50'}`}
>
{item?.description}
{/* <IoIosArrowForward /> */}
</button>
)
})}
</>
}
<>
</>
{selectedLoan.isPending &&
<div className="w-full text-center p-2">
<p className='text-sm'>loading...</p>
</div>
}
{selectedLoan.error &&
<>
<div className="w-full text-center p-2">
<p className='text-red-500 text-sm'>{selectedLoan.error.message}</p>
</div>
</>
}
</div>
</div>
)
}
@@ -0,0 +1,73 @@
import React, { useState } from 'react'
import { useMutation } from '@tanstack/react-query'
import Label from '../../Label'
import InputText from '../../InputText'
import { repayLoan } from '../../../services/siteServices'
export default function PinRepayment({step, handleStep, screens}) {
console.log('step', step.activeUser, step.details)
const [isSuccess, setIsSuccess] = useState(false)
const [error, setError] = useState('')
const [pin, setPin] = useState('')
const handlePinChange = ({target:{value}}) => {
setError('')
setPin(value)
}
const proceed = useMutation({
mutationFn: () => {
let fields = {pin, loan_id:step.details.selected_loan_id, customerID:step.activeUser.customerid}
if(!fields.pin){
throw ({message:'Please enter pin'})
}
if(fields.pin.length != 4){
throw ({message:'Pin must be 4 digits'})
}
return repayLoan(fields)
},
onError: (error) => {
setError(error.message)
setTimeout(()=>{setError('')},3000)
},
onSuccess: (res) => {
setIsSuccess(true)
handleStep({...step}, screens.finished)
}
})
return (
<>
{!isSuccess ?
<div className='mt-3 flex flex-col gap-4'>
<div className='text-input font-semibold w-full flex flex-col gap-4 justify-center items-center'>
<p className='text-center text-base md:text-xl'>{`Your loan amount due is ${step.details.selectedAmount}`}</p>
<Label name='Enter your 4 Digit pin' htmlfor='pin' />
<InputText id='pin' name='pin' type='text' value={pin} handleChange={handlePinChange} />
<button onClick={()=>{proceed.mutate()}} disabled={proceed.isPending} className='p-3 bg-purple-800 text-base sm:text-xl text-white font-bold rounded'>{proceed.isPending ? 'loading...' : 'Continue'}</button>
</div>
</div>
:
<div className='mt-3 flex flex-col gap-4'>
<div className='text-input font-semibold w-full flex flex-col gap-4 justify-center items-center'>
<p className='text-center text-base md:text-xl'>Your loan repayment is being processed. You will receive a confirmation SMS shortly.</p>
</div>
</div>
}
{error &&
<>
<div className="w-full text-center p-2">
<p className='text-red-500 text-sm'>{error}</p>
</div>
</>
}
</>
)
}
@@ -0,0 +1,93 @@
import React, { useState } from 'react'
import { useQuery } from "@tanstack/react-query";
import { useMutation } from '@tanstack/react-query'
import { IoIosArrowForward } from "react-icons/io";
import { getLoanInfo, loanSelect } from '../../../services/siteServices'
import queryKeys from '../../../services/queryKeys'
export default function RepayLoanList({step, handleStep, screens}) {
const [selectedAmount, setSelectedAmount] = useState('')
const {data, isFetching, isError, error} = useQuery({
queryKey: queryKeys.offers,
queryFn: () => getLoanInfo()
})
const loanInfoList = data?.data // LOAN INFO LIST
// const selectedLoan = useMutation({
// mutationFn: (details) => {
// let fields = {bvn:step.activeUser.bvn, loan: details.loan}
// setSelectedAmount(details.amount)
// return loanSelect(fields)
// },
// onError: (error) => {
// },
// onSuccess: (res) => {
// const selectedOffer = res.data.loan
// handleStep({selectedAmount, selectedOffer, ...res.data}, screens.repay_pin)
// }
// })
const handleClick = (selected_loan_id, selectedAmount) => {
handleStep({selected_loan_id, selectedAmount}, screens.repay_pin)
}
return (
<div className='mt-3 flex flex-col gap-4'>
<div className='text-input font-semibold w-full flex flex-col gap-4 justify-center items-center'>
{isFetching ?
<>
<div className="w-full py-4">
<p className='text-slate-800 text-center'>Loading...</p>
</div>
</>
: isError ?
<div className="w-full py-4">
<p className='text-red-500 text-center'>{error.message}</p>
</div>
:
<>
<p className='text-center text-base md:text-xl'>Select Loan to repay</p>
{loanInfoList.map(item => {
let isDisabled = item.active == '0' ? true : false
let amount = item?.description.split(' ')[item?.description.split(' ').length -1] //remove later
return (
<button
key={item?.cid}
// disabled={isDisabled || selectedLoan.isPending}
// value={item.loan}
// onClick={()=>selectedLoan.mutate(item)}
// className={`w-full flex gap-2 justify-between items-center p-2 bg-purple-800 ${selectedLoan.isPending && 'bg-purple-800/50'} text-base sm:text-xl text-white font-bold rounded ${isDisabled && 'opacity-50'}`}
className={`w-full flex gap-2 justify-between items-center p-2 bg-purple-800 text-base sm:text-xl text-white font-bold rounded`}
onClick={()=>handleClick(item.loan_id, amount)}
>
{item?.description}
<IoIosArrowForward />
</button>
)
})}
</>
}
<>
</>
{/* {selectedLoan.isPending &&
<div className="w-full text-center p-2">
<p className='text-sm'>loading...</p>
</div>
}
{selectedLoan.error &&
<>
<div className="w-full text-center p-2">
<p className='text-red-500 text-sm'>{selectedLoan.error.message}</p>
</div>
</>
} */}
</div>
</div>
)
}
+6
View File
@@ -0,0 +1,6 @@
const formatNumber = (number = 0) => {
// return new Intl.NumberFormat().format(number);
return number.toFixed(2);
};
export default formatNumber
+8
View File
@@ -2,6 +2,14 @@
@tailwind components;
@tailwind utilities;
html{
scroll-behavior: smooth;
}
*{
transition: all .3s;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+2 -2
View File
@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom/client';
import {BrowserRouter as Router} from 'react-router-dom'
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
//import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
@@ -17,4 +17,4 @@ root.render(
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
//reportWebVitals();
+2
View File
@@ -1,7 +1,9 @@
const myLinks = {
error: '*',
getStarted: '/',
login: '/login',
home: '/home',
getLoan: '/get-loan'
}
export default myLinks
+8
View File
@@ -0,0 +1,8 @@
import React from 'react'
import LoanScreens from '../components/LoanScreens'
export default function GetLoanPage() {
return (
<LoanScreens />
)
}
+1 -29
View File
@@ -1,37 +1,9 @@
import React, { useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import myLinks from '../myLinks'
import HomeCom from '../components/HomeCom'
import PageLoader from '../components/PageLoader'
export default function HomePage() {
const {state} = useLocation()
const navigate = useNavigate()
const [loading, setLoading] = useState(true)
useEffect(()=>{
if(state?.proceed != 'true'){
return navigate(myLinks.getStarted, {replace:true})
}
setTimeout(()=>{
setLoading(false)
},2000)
},[])
return (
<>
{
loading ?
<PageLoader />
:
<HomeCom />
}
<HomeCom />
</>
)
}
+7
View File
@@ -0,0 +1,7 @@
const queryKeys = {
demoUsers: ['demo-users'],
products: ['products'],
offers: ['offers'],
}
export default queryKeys
+115
View File
@@ -0,0 +1,115 @@
import axios from "axios"
axios.interceptors.request.use(
config => {
config.headers = {
Accept: "application/json",
"Access-Control-Allow-Origin": "*",
// "Access-Control-Expose-Headers": "Access-Control-Allow-Origin",
// "Access-Control-Allow-Headers": "Origin, X-API-KEY, X-Requested-With, Content-Type, Accept, Access-Control-Request-Method, Access-Control-Allow-Headers, Authorization, observe, enctype, Content-Length, X-Csrf-Token",
"Content-Type": "application/json;charset=UTF-8",
// 'Authorization': `Bearer ${localStorage.getItem('token')}`
};
// config.headers['Authorization'] = `Bearer ${localStorage.getItem('token')}`;
// config.baseURL = process.env.REACT_APP_MAIN_API
return config;
},
error => {
return Promise.reject(error);
}
);
const postAuxEnd = (path, postData, media=false) => {
const basePath = media ? process.env.REACT_APP_MAIN_API : process.env.REACT_APP_MAIN_API
return axios.post(`${basePath}${path}`, postData).then(res => {
return res
}).catch(err => {
throw new Error(err.response.data.message);
})
}
const getAuxEnd = (path, reqData= null) => {
const basePath = process.env.REACT_APP_MAIN_API
return axios.get(`${basePath}${path}`,{ params: reqData }).then(res => {
return res
// localStorage.clear();
// window.location.href = `/login?sessionExpired=true`;
}).catch(err => {
throw new Error(err);
// throw new Error(err.response.data.message);
// return err
})
}
// FUNCTION TO LOGIN USER IN
export const loginUser = (reqData) => {
let postData = {
...reqData
}
return postAuxEnd('/salary/login', postData, false)
}
// FUNCTION TO LOGIN USER IN
export const verifyPin = (reqData) => {
let postData = {
...reqData
}
return postAuxEnd('/salary/verifypin', postData, false)
}
// FUNCTION TO VERIFY LOAN
export const verifyLoan = (reqData) => {
let postData = {
...reqData
}
return postAuxEnd('/salary/verifloan', postData, false)
}
// FUNCTION TO REPAY LOAN
export const repayLoan = (reqData) => {
let postData = {
...reqData
}
return postAuxEnd('/loan/repay', postData, false)
}
// FUNCTION TO RUN WHEN USER SELECTS A LOAN
export const loanSelect = (reqData) => {
let postData = {
...reqData
}
return postAuxEnd('/salary/loanselect', postData, false)
}
// FUNCTION TO APPLY FOR LOAN
export const loanApply = (reqData) => {
let postData = {
...reqData
}
return postAuxEnd('/salary/loanapply', postData, false)
}
// FUNCTION TO GET DEMO USERS
export const demoUsersList = (reqData) => {
const postData = { ...reqData }
return getAuxEnd(`/salary/demousers`, postData)
}
// FUNCTION TO GET MY PRODUCTS DATA
export const getProducts = (reqData) => {
const postData = { ...reqData }
return getAuxEnd(`/salary/products`, postData)
}
// FUNCTION TO GET MY OFFERS DATA
export const getOffers = (reqData) => {
const postData = { ...reqData }
return getAuxEnd(`/salary/loanoffers`, postData)
}
// FUNCTION TO GET LOAN INFO
export const getLoanInfo = (reqData) => {
const postData = { ...reqData }
return getAuxEnd(`/loan/info`, postData)
}
+20
View File
@@ -0,0 +1,20 @@
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
userDetails: {},
};
export const userSlice = createSlice({
name: "userDetails",
initialState,
reducers: {
updateUserDetails: (state, action) => {
state.userDetails = { ...action.payload };
},
},
});
// Action creators are generated for each case reducer function
export const { updateUserDetails } = userSlice.actions;
export default userSlice.reducer;
+10
View File
@@ -0,0 +1,10 @@
import { configureStore } from "@reduxjs/toolkit";
import userDetailReducer from "./UserDetails";
export default configureStore({
reducer: {
userDetails: userDetailReducer,
},
});