From 3cdc49d5768320279dd29c9f220a7a014f376986 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 29 Mar 2026 09:24:23 +0000 Subject: [PATCH] Jk --- backend/src/index.js | 2 + backend/src/routes/public_forms.js | 203 ++++++ frontend/src/components/NavBarItem.tsx | 3 +- frontend/src/data/jkMicrofinance.ts | 113 +++ frontend/src/layouts/Authenticated.tsx | 3 +- frontend/src/pages/apply.tsx | 443 ++++++++++++ frontend/src/pages/index.tsx | 960 +++++++++++++++++++++---- 7 files changed, 1581 insertions(+), 146 deletions(-) create mode 100644 backend/src/routes/public_forms.js create mode 100644 frontend/src/data/jkMicrofinance.ts create mode 100644 frontend/src/pages/apply.tsx diff --git a/backend/src/index.js b/backend/src/index.js index 597b04a..77e38dd 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -16,6 +16,7 @@ const fileRoutes = require('./routes/file'); const searchRoutes = require('./routes/search'); const sqlRoutes = require('./routes/sql'); const pexelsRoutes = require('./routes/pexels'); +const publicFormsRoutes = require('./routes/public_forms'); const openaiRoutes = require('./routes/openai'); @@ -100,6 +101,7 @@ app.use(bodyParser.json()); app.use('/api/auth', authRoutes); app.use('/api/file', fileRoutes); app.use('/api/pexels', pexelsRoutes); +app.use('/api/public/forms', publicFormsRoutes); app.enable('trust proxy'); diff --git a/backend/src/routes/public_forms.js b/backend/src/routes/public_forms.js new file mode 100644 index 0000000..fe94dcb --- /dev/null +++ b/backend/src/routes/public_forms.js @@ -0,0 +1,203 @@ +const express = require('express'); + +const db = require('../db/models'); +const Loan_applicationsDBApi = require('../db/api/loan_applications'); +const Contact_messagesDBApi = require('../db/api/contact_messages'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const CONTACT_SUBJECTS = new Set([ + 'loan_application_help', + 'loan_terms', + 'repayment', + 'collateral', + 'partnership', + 'other', +]); + +const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +const phonePattern = /^[+()\d\s-]{7,20}$/; + +function createBadRequest(message) { + const error = new Error(message); + error.code = 400; + return error; +} + +function cleanText(value) { + return typeof value === 'string' ? value.trim() : ''; +} + +function requireText(value, label, minLength = 2) { + const cleaned = cleanText(value); + + if (cleaned.length < minLength) { + throw createBadRequest(`${label} is required.`); + } + + return cleaned; +} + +function requirePositiveNumber(value, label) { + const parsed = Number(value); + + if (!Number.isFinite(parsed) || parsed <= 0) { + throw createBadRequest(`${label} must be a valid amount.`); + } + + return parsed; +} + +function requireEmail(value) { + const cleaned = requireText(value, 'Email'); + + if (!emailPattern.test(cleaned)) { + throw createBadRequest('Please provide a valid email address.'); + } + + return cleaned; +} + +function requirePhone(value) { + const cleaned = requireText(value, 'Phone number'); + + if (!phonePattern.test(cleaned)) { + throw createBadRequest('Please provide a valid phone number.'); + } + + return cleaned; +} + +function buildApplicationNumber() { + const now = new Date(); + const datePart = `${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(2, '0')}${String(now.getUTCDate()).padStart(2, '0')}`; + const randomPart = Math.random().toString(36).slice(2, 6).toUpperCase(); + + return `JKM-${datePart}-${randomPart}`; +} + +router.post('/apply', wrapAsync(async (req, res) => { + const payload = req.body?.data || req.body || {}; + + const fullName = requireText(payload.fullName, 'Full name'); + const university = requireText(payload.university, 'University'); + const phone = requirePhone(payload.phone); + const email = requireEmail(payload.email); + const purpose = requireText(payload.purpose, 'Purpose', 4); + const collateralItem = requireText(payload.collateralItem, 'Collateral item', 3); + const requestedAmount = requirePositiveNumber(payload.requestedAmount, 'Requested amount'); + const collateralValue = requirePositiveNumber(payload.collateralValue, 'Collateral value'); + const consent = Boolean(payload.consent); + + if (!consent) { + throw createBadRequest('You must confirm that you understand the loan terms before submitting.'); + } + + if (collateralValue < requestedAmount * 2) { + throw createBadRequest('Collateral value must be at least 2× the requested loan amount.'); + } + + const submittedAt = new Date(); + const applicationNumber = buildApplicationNumber(); + const currentUser = { id: null }; + const applicationSummary = `${purpose} — ${university}`; + const internalNote = [ + `Applicant: ${fullName}`, + `University: ${university}`, + `Phone: ${phone}`, + `Email: ${email}`, + `Collateral item: ${collateralItem}`, + `Collateral value: ${collateralValue}`, + 'Terms acknowledged: 2 weeks, 25% interest, collateral held as security.', + ].join('\n'); + + const transaction = await db.sequelize.transaction(); + + try { + const loanApplication = await Loan_applicationsDBApi.create( + { + application_number: applicationNumber, + requested_amount: requestedAmount, + purpose: applicationSummary, + status: 'submitted', + submitted_at: submittedAt, + decision_note: internalNote, + }, + { + currentUser, + transaction, + }, + ); + + await Contact_messagesDBApi.create( + { + sender_name: fullName, + sender_email: email, + sender_phone: phone, + subject: 'loan_application_help', + message: [`Loan application reference: ${applicationNumber}`, internalNote].join('\n\n'), + status: 'new', + received_at: submittedAt, + }, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + + res.status(200).send({ + success: true, + reference: applicationNumber, + loanApplicationId: loanApplication.id, + message: 'Application received successfully.', + }); + } catch (error) { + await transaction.rollback(); + throw error; + } +})); + +router.post('/contact', wrapAsync(async (req, res) => { + const payload = req.body?.data || req.body || {}; + + const senderName = requireText(payload.name, 'Name'); + const senderEmail = requireEmail(payload.email); + const senderPhone = cleanText(payload.phone); + const message = requireText(payload.message, 'Message', 8); + const subject = cleanText(payload.subject) || 'other'; + + if (senderPhone && !phonePattern.test(senderPhone)) { + throw createBadRequest('Please provide a valid phone number.'); + } + + if (!CONTACT_SUBJECTS.has(subject)) { + throw createBadRequest('Please choose a valid subject.'); + } + + await Contact_messagesDBApi.create( + { + sender_name: senderName, + sender_email: senderEmail, + sender_phone: senderPhone || null, + subject, + message, + status: 'new', + received_at: new Date(), + }, + { + currentUser: { id: null }, + }, + ); + + res.status(200).send({ + success: true, + message: 'Message sent successfully.', + }); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index 72935e6..fcbd9b9 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -1,6 +1,5 @@ -import React, {useEffect, useRef} from 'react' +import React, { useEffect, useRef, useState } from 'react' import Link from 'next/link' -import { useState } from 'react' import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import BaseDivider from './BaseDivider' import BaseIcon from './BaseIcon' diff --git a/frontend/src/data/jkMicrofinance.ts b/frontend/src/data/jkMicrofinance.ts new file mode 100644 index 0000000..09d44ea --- /dev/null +++ b/frontend/src/data/jkMicrofinance.ts @@ -0,0 +1,113 @@ +export const brand = { + name: 'JK Microfinance', + location: 'Dar es Salaam, Tanzania', + phone: '+255700123456', + email: 'info@jkmicrofinance.co.tz', + primary: '#0B1F4B', + accent: '#D4AF37', + mutedBg: '#F5F8FC', + headline: 'Quick Loans for University Students in Dar es Salaam', + subheadline: 'Get the cash you need in minutes — simple, fast, and reliable.', +}; + +export const navigationLinks = [ + { label: 'About', href: '#about' }, + { label: 'Loan Terms', href: '#loan-terms' }, + { label: 'How It Works', href: '#how-it-works' }, + { label: 'Testimonials', href: '#testimonials' }, + { label: 'Contact', href: '#contact' }, +]; + +export const trustPoints = [ + 'Built for Dar es Salaam university students', + 'Same-day review with clear 2-week repayment terms', + 'Secure collateral handling and transparent pricing', +]; + +export const services = [ + { + title: 'Student Emergency Loans', + description: + 'Fast support for urgent school payments, medical needs, or family emergencies when timing matters most.', + }, + { + title: 'Short-Term Cash Loans', + description: + 'Simple 2-week loans designed for short gaps between allowances, support from home, or side-income payments.', + }, + { + title: 'Quick Approval Loans', + description: + 'A streamlined review process with student-friendly guidance so you can understand your terms before you commit.', + }, +]; + +export const reasons = [ + 'Fast approval, often the same day', + 'Student-focused support for university life', + 'Simple and transparent requirements', + 'Trusted, secure handling of collateral', + 'Convenient for students based in Dar es Salaam', +]; + +export const processSteps = [ + { + step: '1', + title: 'Apply for a loan', + description: + 'Tell us how much you need, what it is for, and how we can reach you quickly.', + }, + { + step: '2', + title: 'Provide collateral worth 2× the loan value', + description: + 'We safely hold a valuable item as bond while your short-term loan is active.', + }, + { + step: '3', + title: 'Receive your money quickly', + description: + 'Once everything is confirmed, we arrange your funds without unnecessary delays.', + }, +]; + +export const testimonials = [ + { + name: 'Hawa Selemani', + university: 'University of Dar es Salaam', + quote: + 'I was short on my registration payment and JK helped me cover it quickly with clear terms.', + }, + { + name: 'Baraka Mwinuka', + university: 'Institute of Finance Management', + quote: + 'I needed urgent cash for a family emergency and the process was straightforward and respectful.', + }, + { + name: 'Rehema Salum', + university: 'Ardhi University', + quote: + 'During exams I needed support for transport and meals. Approval was fast and the terms were clear.', + }, + { + name: 'Elvis Mrema', + university: 'Muhimbili University of Health and Allied Sciences', + quote: + 'Same-day support when I had clinical rotation costs. Communication was professional throughout.', + }, +]; + +export const transparencyPoints = [ + 'Each loan runs for 2 weeks from the date you receive your funds.', + 'A 25% interest charge applies for each loan period.', + 'Collateral worth at least 2× the loan amount is required before disbursement.', + 'Collateral is stored securely and is only used for recovery if repayment is delayed.', +]; + +export const campusImageQueries = [ + 'african university students', + 'african campus life', + 'student studying africa', + 'young african professionals', +]; diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..73d8391 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -1,5 +1,4 @@ -import React, { ReactNode, useEffect } from 'react' -import { useState } from 'react' +import React, { ReactNode, useEffect, useState } from 'react' import jwt from 'jsonwebtoken'; import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import menuAside from '../menuAside' diff --git a/frontend/src/pages/apply.tsx b/frontend/src/pages/apply.tsx new file mode 100644 index 0000000..77abf83 --- /dev/null +++ b/frontend/src/pages/apply.tsx @@ -0,0 +1,443 @@ +import { + mdiArrowLeft, + mdiCheckCircle, + mdiShieldCheck, + mdiTimerSand, +} from '@mdi/js'; +import axios from 'axios'; +import Head from 'next/head'; +import Link from 'next/link'; +import React, { ReactElement, useMemo, useState } from 'react'; +import BaseButton from '../components/BaseButton'; +import BaseIcon from '../components/BaseIcon'; +import { getPageTitle } from '../config'; +import { brand, transparencyPoints } from '../data/jkMicrofinance'; +import LayoutGuest from '../layouts/Guest'; + +type ApplyFormState = { + fullName: string; + university: string; + phone: string; + email: string; + requestedAmount: string; + purpose: string; + collateralItem: string; + collateralValue: string; + consent: boolean; +}; + +type ApplyResponse = { + reference: string; +}; + +const initialFormState: ApplyFormState = { + fullName: '', + university: '', + phone: '', + email: '', + requestedAmount: '', + purpose: '', + collateralItem: '', + collateralValue: '', + consent: false, +}; + +const formatCurrency = (value: number) => + new Intl.NumberFormat('en-TZ', { + style: 'currency', + currency: 'TZS', + maximumFractionDigits: 0, + }).format(value || 0); + +function getErrorMessage(error: unknown) { + if (axios.isAxiosError(error)) { + if (typeof error.response?.data === 'string') { + return error.response.data; + } + + return error.message; + } + + if (error instanceof Error) { + return error.message; + } + + return 'We could not send your application right now. Please try again.'; +} + +export default function ApplyPage() { + const [form, setForm] = useState(initialFormState); + const [errorMessage, setErrorMessage] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const [successData, setSuccessData] = useState(null); + + const requestedAmount = Number(form.requestedAmount) || 0; + const collateralValue = Number(form.collateralValue) || 0; + + const repaymentAmount = useMemo( + () => requestedAmount * 1.25, + [requestedAmount], + ); + const minimumCollateral = useMemo( + () => requestedAmount * 2, + [requestedAmount], + ); + + const updateField = ( + event: + | React.ChangeEvent + | React.ChangeEvent + | React.ChangeEvent, + ) => { + const target = event.target; + const { name, value } = target; + const nextValue = + target instanceof HTMLInputElement && target.type === 'checkbox' + ? target.checked + : value; + + setForm((current) => ({ + ...current, + [name]: nextValue, + })); + }; + + const validateForm = () => { + if (!form.fullName.trim()) return 'Please enter your full name.'; + if (!form.university.trim()) return 'Please enter your university name.'; + if (!form.phone.trim()) return 'Please enter your phone number.'; + if (!form.email.trim()) return 'Please enter your email address.'; + if (!requestedAmount || requestedAmount <= 0) + return 'Please enter the amount you want to borrow.'; + if (!form.purpose.trim()) return 'Please tell us what the loan will help with.'; + if (!form.collateralItem.trim()) + return 'Please describe the collateral item you will provide.'; + if (!collateralValue || collateralValue <= 0) + return 'Please enter the estimated value of your collateral.'; + if (collateralValue < minimumCollateral) + return 'Your collateral must be worth at least 2× the requested amount.'; + if (!form.consent) + return 'Please confirm that you understand the 2-week term, 25% interest, and collateral requirement.'; + + return ''; + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + const validationMessage = validateForm(); + + if (validationMessage) { + setErrorMessage(validationMessage); + return; + } + + try { + setIsSubmitting(true); + setErrorMessage(''); + + const response = await axios.post('/public/forms/apply', { + data: form, + }); + + setSuccessData({ reference: response.data.reference }); + setForm(initialFormState); + } catch (error: unknown) { + setErrorMessage(getErrorMessage(error)); + } finally { + setIsSubmitting(false); + } + }; + + return ( + <> + + {getPageTitle('Apply for a student loan')} + +
+
+
+ + + Back to home + +
+ +
+
+
+ +
+
+
+
+ + Student-focused support in Dar es Salaam + +
+

+ Apply in minutes and see your loan terms before you submit. +

+

+ {brand.name} is built for university students who need short-term financial support without hidden terms. + Share your request, confirm the collateral details, and our team will follow up quickly. +

+
+ +
+
+
+ +
+

2-week loan period

+

Designed for urgent, short-term student needs.

+
+
+
+ +
+

25% interest per loan

+

The repayment amount is shown clearly before you apply.

+
+
+
+ +
+

Collateral required

+

Your item must be worth at least 2× the amount requested.

+
+
+ +
+

Transparent terms

+
    + {transparencyPoints.map((point) => ( +
  • + + {point} +
  • + ))} +
+
+
+ +
+ {successData ? ( +
+
+ +
+
+

+ Application received +

+

+ Thank you — we’ve recorded your request. +

+

+ Your reference is {successData.reference}. Keep it handy if you speak to our team. +

+
+
+

What happens next?

+
    +
  • • Our team reviews your request and confirms your contact details.
  • +
  • • We verify the collateral item and explain the repayment amount.
  • +
  • • If everything is in order, we arrange the next step quickly.
  • +
+
+
+ + setSuccessData(null)} + /> +
+
+ ) : ( + <> +
+

+ Start your application +

+

+ Student loan request form +

+

+ Fill in your details and we’ll record your request with clear terms attached. +

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + +