From f7560ce59529d768e726338bf1c708e7ef957e42 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 10 Feb 2026 21:42:28 +0000 Subject: [PATCH] Auto commit: 2026-02-10T21:42:28.394Z --- backend/src/db/api/users.js | 62 +- backend/src/db/migrations/1770756403378.js | 82 ++ backend/src/db/models/users.js | 77 +- backend/src/index.js | 7 +- backend/src/routes/landing.js | 21 + backend/src/routes/wallet.js | 66 ++ frontend/src/components/NavBar.tsx | 16 +- frontend/src/components/NavBarItem.tsx | 5 +- .../src/components/UserAvatarWithBadge.tsx | 48 + frontend/src/components/WalletCard.tsx | 95 ++ frontend/src/layouts/Authenticated.tsx | 5 +- frontend/src/pages/dashboard.tsx | 887 ++++-------------- frontend/src/pages/index.tsx | 555 ++++++++--- frontend/src/pages/profile.tsx | 164 ++-- 14 files changed, 1111 insertions(+), 979 deletions(-) create mode 100644 backend/src/db/migrations/1770756403378.js create mode 100644 backend/src/routes/landing.js create mode 100644 backend/src/routes/wallet.js create mode 100644 frontend/src/components/UserAvatarWithBadge.tsx create mode 100644 frontend/src/components/WalletCard.tsx diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js index 63e8eab..57b5668 100644 --- a/backend/src/db/api/users.js +++ b/backend/src/db/api/users.js @@ -1,4 +1,3 @@ - const db = require('../models'); const FileDBApi = require('./file'); const crypto = require('crypto'); @@ -32,7 +31,12 @@ module.exports = class UsersDBApi { || null , - + national_id: data.data.national_id || null, + address: data.data.address || null, + birth_day: data.data.birth_day || null, + birth_year: data.data.birth_year || null, + account_type: data.data.account_type || null, + phoneNumber: data.data.phoneNumber || null @@ -109,7 +113,12 @@ module.exports = class UsersDBApi { }); } - + await users.setAccount_tier(data.data.account_tier || null, { transaction }); + await users.setProvince(data.data.province || null, { transaction }); + await users.setDistrict(data.data.district || null, { transaction }); + await users.setSector(data.data.sector || null, { transaction }); + await users.setCell(data.data.cell || null, { transaction }); + await users.setVillage(data.data.village || null, { transaction }); await users.setCustom_permissions(data.data.custom_permissions || [], { @@ -262,43 +271,27 @@ module.exports = class UsersDBApi { const updatePayload = {}; if (data.firstName !== undefined) updatePayload.firstName = data.firstName; - - if (data.lastName !== undefined) updatePayload.lastName = data.lastName; - - + if (data.national_id !== undefined) updatePayload.national_id = data.national_id; + if (data.address !== undefined) updatePayload.address = data.address; + if (data.birth_day !== undefined) updatePayload.birth_day = data.birth_day; + if (data.birth_year !== undefined) updatePayload.birth_year = data.birth_year; + if (data.account_type !== undefined) updatePayload.account_type = data.account_type; + if (data.phoneNumber !== undefined) updatePayload.phoneNumber = data.phoneNumber; - - if (data.email !== undefined) updatePayload.email = data.email; - - if (data.disabled !== undefined) updatePayload.disabled = data.disabled; - - if (data.password !== undefined) updatePayload.password = data.password; - if (data.emailVerified !== undefined) updatePayload.emailVerified = data.emailVerified; - else updatePayload.emailVerified = true; - if (data.emailVerificationToken !== undefined) updatePayload.emailVerificationToken = data.emailVerificationToken; - - if (data.emailVerificationTokenExpiresAt !== undefined) updatePayload.emailVerificationTokenExpiresAt = data.emailVerificationTokenExpiresAt; - - if (data.passwordResetToken !== undefined) updatePayload.passwordResetToken = data.passwordResetToken; - - if (data.passwordResetTokenExpiresAt !== undefined) updatePayload.passwordResetTokenExpiresAt = data.passwordResetTokenExpiresAt; - - if (data.provider !== undefined) updatePayload.provider = data.provider; - updatePayload.updatedById = currentUser.id; await users.update(updatePayload, {transaction}); @@ -313,6 +306,13 @@ module.exports = class UsersDBApi { { transaction } ); } + + if (data.account_tier !== undefined) await users.setAccount_tier(data.account_tier, { transaction }); + if (data.province !== undefined) await users.setProvince(data.province, { transaction }); + if (data.district !== undefined) await users.setDistrict(data.district, { transaction }); + if (data.sector !== undefined) await users.setSector(data.sector, { transaction }); + if (data.cell !== undefined) await users.setCell(data.cell, { transaction }); + if (data.village !== undefined) await users.setVillage(data.village, { transaction }); @@ -363,7 +363,7 @@ module.exports = class UsersDBApi { }); - return users; + return posts; } static async remove(id, options) { @@ -462,6 +462,13 @@ module.exports = class UsersDBApi { output.app_role = await users.getApp_role({ transaction }); + + output.account_tier = await users.getAccount_tier({ transaction }); + output.province = await users.getProvince({ transaction }); + output.district = await users.getDistrict({ transaction }); + output.sector = await users.getSector({ transaction }); + output.cell = await users.getCell({ transaction }); + output.village = await users.getVillage({ transaction }); if (output.app_role) { output.app_role_permissions = await output.app_role.getPermissions({ @@ -973,5 +980,4 @@ module.exports = class UsersDBApi { -}; - +}; \ No newline at end of file diff --git a/backend/src/db/migrations/1770756403378.js b/backend/src/db/migrations/1770756403378.js new file mode 100644 index 0000000..9957886 --- /dev/null +++ b/backend/src/db/migrations/1770756403378.js @@ -0,0 +1,82 @@ +module.exports = { + async up(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'users', + 'national_id', + { + type: Sequelize.DataTypes.TEXT, + allowNull: true, + }, + { transaction } + ); + await queryInterface.addColumn( + 'users', + 'address', + { + type: Sequelize.DataTypes.TEXT, + allowNull: true, + }, + { transaction } + ); + await queryInterface.addColumn( + 'users', + 'birth_day', + { + type: Sequelize.DataTypes.INTEGER, + allowNull: true, + }, + { transaction } + ); + await queryInterface.addColumn( + 'users', + 'birth_year', + { + type: Sequelize.DataTypes.INTEGER, + allowNull: true, + }, + { transaction } + ); + await queryInterface.addColumn( + 'users', + 'account_type', + { + type: Sequelize.DataTypes.ENUM('Worker', 'Employer', 'Business', 'Government', 'NGO'), + allowNull: true, + }, + { transaction } + ); + await queryInterface.addColumn( + 'users', + 'wallet_balance_rwf', + { + type: Sequelize.DataTypes.DECIMAL, + defaultValue: 0, + allowNull: false, + }, + { transaction } + ); + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + + async down(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('users', 'national_id', { transaction }); + await queryInterface.removeColumn('users', 'address', { transaction }); + await queryInterface.removeColumn('users', 'birth_day', { transaction }); + await queryInterface.removeColumn('users', 'birth_year', { transaction }); + await queryInterface.removeColumn('users', 'account_type', { transaction }); + await queryInterface.removeColumn('users', 'wallet_balance_rwf', { transaction }); + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; \ No newline at end of file diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js index 0305f10..9994d0e 100644 --- a/backend/src/db/models/users.js +++ b/backend/src/db/models/users.js @@ -28,6 +28,27 @@ lastName: { }, +national_id: { + type: DataTypes.TEXT, + }, + +address: { + type: DataTypes.TEXT, + }, + +birth_day: { + type: DataTypes.INTEGER, + }, + +birth_year: { + type: DataTypes.INTEGER, + }, + +account_type: { + type: DataTypes.ENUM, + values: ['Worker', 'Employer', 'Business', 'Government', 'NGO'], + }, + phoneNumber: { type: DataTypes.TEXT, @@ -104,6 +125,12 @@ provider: { }, +wallet_balance_rwf: { + type: DataTypes.DECIMAL, + allowNull: false, + defaultValue: 0, + }, + importHash: { type: DataTypes.STRING(255), allowNull: true, @@ -239,6 +266,53 @@ provider: { constraints: false, }); + db.users.belongsTo(db.account_tiers, { + as: 'account_tier', + foreignKey: { + name: 'account_tierId', + }, + constraints: false, + }); + + db.users.belongsTo(db.provinces, { + as: 'province', + foreignKey: { + name: 'provinceId', + }, + constraints: false, + }); + + db.users.belongsTo(db.districts, { + as: 'district', + foreignKey: { + name: 'districtId', + }, + constraints: false, + }); + + db.users.belongsTo(db.sectors, { + as: 'sector', + foreignKey: { + name: 'sectorId', + }, + constraints: false, + }); + + db.users.belongsTo(db.cells, { + as: 'cell', + foreignKey: { + name: 'cellId', + }, + constraints: false, + }); + + db.users.belongsTo(db.villages, { + as: 'village', + foreignKey: { + name: 'villageId', + }, + constraints: false, + }); db.users.hasMany(db.file, { @@ -304,5 +378,4 @@ function trimStringFields(users) { : null; return users; -} - +} \ No newline at end of file diff --git a/backend/src/index.js b/backend/src/index.js index 648d2e8..2966143 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -1,4 +1,3 @@ - const express = require('express'); const cors = require('cors'); const app = express(); @@ -63,6 +62,7 @@ const help_center_articlesRoutes = require('./routes/help_center_articles'); const for_you_rankingsRoutes = require('./routes/for_you_rankings'); +const walletRoutes = require('./routes/wallet'); const getBaseUrl = (url) => { if (!url) return ''; @@ -118,6 +118,7 @@ app.use(bodyParser.json()); app.use('/api/auth', authRoutes); app.use('/api/file', fileRoutes); app.use('/api/pexels', pexelsRoutes); +app.use('/api/landing', require('./routes/landing')); app.enable('trust proxy'); @@ -163,6 +164,8 @@ app.use('/api/help_center_articles', passport.authenticate('jwt', {session: fals app.use('/api/for_you_rankings', passport.authenticate('jwt', {session: false}), for_you_rankingsRoutes); +app.use('/api/wallet', walletRoutes); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), @@ -207,4 +210,4 @@ db.sequelize.sync().then(function () { }); }); -module.exports = app; +module.exports = app; \ No newline at end of file diff --git a/backend/src/routes/landing.js b/backend/src/routes/landing.js new file mode 100644 index 0000000..324d38a --- /dev/null +++ b/backend/src/routes/landing.js @@ -0,0 +1,21 @@ + +const express = require('express'); +const router = express.Router(); +const PostsDBApi = require('../db/api/posts'); +const AccountTiersDBApi = require('../db/api/account_tiers'); +const wrapAsync = require('../helpers').wrapAsync; + +router.get('/data', wrapAsync(async (req, res) => { + // Fetch latest posts for guest users + const posts = await PostsDBApi.findAll({ limit: 6, page: 0 }, { currentUser: null }); + + // Fetch account tiers + const tiers = await AccountTiersDBApi.findAll({ limit: 10, page: 0 }, { currentUser: null }); + + res.status(200).send({ + posts: posts.rows, + tiers: tiers.rows + }); +})); + +module.exports = router; diff --git a/backend/src/routes/wallet.js b/backend/src/routes/wallet.js new file mode 100644 index 0000000..901119c --- /dev/null +++ b/backend/src/routes/wallet.js @@ -0,0 +1,66 @@ +const express = require('express'); +const router = express.Router(); +const passport = require('passport'); +const db = require('../db/models'); +const { wrapAsync } = require('../helpers'); + +router.post( + '/deposit', + passport.authenticate('jwt', { session: false }), + wrapAsync(async (req, res) => { + const { amount } = req.body; + const userId = req.user.id; + + if (!amount || isNaN(amount) || amount <= 0) { + return res.status(400).send({ message: 'Invalid amount' }); + } + + const transaction = await db.sequelize.transaction(); + + try { + const user = await db.users.findByPk(userId, { transaction }); + + const balanceBefore = parseFloat(user.wallet_balance_rwf || 0); + const depositAmount = parseFloat(amount); + const balanceAfter = balanceBefore + depositAmount; + + await user.update({ wallet_balance_rwf: balanceAfter }, { transaction }); + + await db.wallet_transactions.create({ + userId, + type: 'deposit', + status: 'completed', + amount_rwf: depositAmount, + balance_before_rwf: balanceBefore, + balance_after_rwf: balanceAfter, + reference_code: `DEP-${Date.now()}`, + reason: 'Manual simulation deposit', + processed_at: new Date(), + createdById: userId, + updatedById: userId, + }, { transaction }); + + await transaction.commit(); + + res.status(200).send({ + message: 'Deposit successful', + balance: balanceAfter + }); + } catch (error) { + await transaction.rollback(); + console.error('Deposit error:', error); + res.status(500).send({ message: 'Internal server error' }); + } + }) +); + +router.get( + '/balance', + passport.authenticate('jwt', { session: false }), + wrapAsync(async (req, res) => { + const user = await db.users.findByPk(req.user.id); + res.status(200).send({ balance: user.wallet_balance_rwf }); + }) +); + +module.exports = router; diff --git a/frontend/src/components/NavBar.tsx b/frontend/src/components/NavBar.tsx index c270ae0..455120f 100644 --- a/frontend/src/components/NavBar.tsx +++ b/frontend/src/components/NavBar.tsx @@ -1,11 +1,12 @@ import React, { ReactNode, useState, useEffect } from 'react' -import { mdiClose, mdiDotsVertical } from '@mdi/js' +import { mdiClose, mdiDotsVertical, mdiWallet } from '@mdi/js' import { containerMaxW } from '../config' import BaseIcon from './BaseIcon' import NavBarItemPlain from './NavBarItemPlain' import NavBarMenuList from './NavBarMenuList' import { MenuNavBarItem } from '../interfaces' import { useAppSelector } from '../stores/hooks'; +import Link from 'next/link'; type Props = { menu: MenuNavBarItem[] @@ -17,6 +18,7 @@ export default function NavBar({ menu, className = '', children }: Props) { const [isMenuNavBarActive, setIsMenuNavBarActive] = useState(false) const [isScrolled, setIsScrolled] = useState(false); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const { currentUser } = useAppSelector((state) => state.auth); useEffect(() => { const handleScroll = () => { @@ -39,6 +41,16 @@ export default function NavBar({ menu, className = '', children }: Props) { >
{children}
+ + {currentUser && ( +
+ + + 0 RWF + +
+ )} +
@@ -54,4 +66,4 @@ export default function NavBar({ menu, className = '', children }: Props) {
) -} +} \ No newline at end of file diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index 72935e6..4ced3eb 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' @@ -129,4 +128,4 @@ export default function NavBarItem({ item }: Props) { } return
{NavBarItemComponentContents}
-} +} \ No newline at end of file diff --git a/frontend/src/components/UserAvatarWithBadge.tsx b/frontend/src/components/UserAvatarWithBadge.tsx new file mode 100644 index 0000000..2e93def --- /dev/null +++ b/frontend/src/components/UserAvatarWithBadge.tsx @@ -0,0 +1,48 @@ + +import React from 'react'; +import { mdiAccount, mdiImageOutline } from '@mdi/js'; +import BaseIcon from './BaseIcon'; + +type Props = { + user: any; + className?: string; + size?: number; +}; + +export default function UserAvatarWithBadge({ user, className = '', size = 48 }: Props) { + const avatarUrl = user?.avatar && user.avatar[0] ? user.avatar[0].publicUrl : null; + const tierName = user?.account_tier?.name?.toLowerCase() || ''; + + const getBadgeColor = (name: string) => { + if (name.includes('plutonium')) return 'bg-purple-600'; + if (name.includes('platinum')) return 'bg-blue-600'; + if (name.includes('gold')) return 'bg-yellow-500'; + if (name.includes('silver')) return 'bg-slate-400'; + if (name.includes('bronze')) return 'bg-orange-500'; + return null; + }; + + const badgeColor = getBadgeColor(tierName); + + return ( +
+ {avatarUrl ? ( + {user?.firstName + ) : ( +
+ +
+ )} + + {badgeColor && ( +
+ {user.account_tier.name.substring(0, 1)} +
+ )} +
+ ); +} diff --git a/frontend/src/components/WalletCard.tsx b/frontend/src/components/WalletCard.tsx new file mode 100644 index 0000000..d30fcf5 --- /dev/null +++ b/frontend/src/components/WalletCard.tsx @@ -0,0 +1,95 @@ +import React, { useState, useEffect } from 'react'; +import axios from 'axios'; +import { mdiWallet, mdiPlusCircle } from '@mdi/js'; +import BaseIcon from './BaseIcon'; +import CardBox from './CardBox'; +import BaseButton from './BaseButton'; +import CardBoxModal from './CardBoxModal'; +import FormField from './FormField'; + +const WalletCard = () => { + const [balance, setBalance] = useState(0); + const [isModalOpen, setIsModalOpen] = useState(false); + const [depositAmount, setDepositAmount] = useState('1000'); + const [isLoading, setIsLoading] = useState(false); + + const fetchBalance = async () => { + try { + const response = await axios.get('/wallet/balance'); + setBalance(Number(response.data.balance) || 0); + } catch (error) { + console.error('Failed to fetch balance:', error); + } + }; + + useEffect(() => { + fetchBalance(); + }, []); + + const handleDeposit = async () => { + setIsLoading(true); + try { + await axios.post('/wallet/deposit', { amount: Number(depositAmount) }); + await fetchBalance(); + setIsModalOpen(false); + } catch (error) { + console.error('Deposit failed:', error); + alert('Deposit failed. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + return ( + <> + setIsModalOpen(false)} + isLoading={isLoading} + > +

+ This is a Wallet Simulation. No real money will be charged. + Enter the amount in RWF you want to add to your MENYAHAFI account. +

+ + setDepositAmount(e.target.value)} + className="px-3 py-2 max-w-full focus:ring focus:outline-none border-gray-700 rounded w-full bg-white" + /> + +
+ +
+
+
+
+ +
+
+

Your Balance

+

+ {balance.toLocaleString()} RWF +

+
+
+ +
+
+ + ); +}; + +export default WalletCard; diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..26c3572 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' @@ -126,4 +125,4 @@ export default function LayoutAuthenticated({
) -} +} \ No newline at end of file diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index 7bd442e..d7ff4d2 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -11,748 +11,195 @@ import { getPageTitle } from '../config' import Link from "next/link"; import { hasPermission } from "../helpers/userPermissions"; -import { fetchWidgets } from '../stores/roles/rolesSlice'; -import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator'; -import { SmartWidget } from '../components/SmartWidget/SmartWidget'; - import { useAppDispatch, useAppSelector } from '../stores/hooks'; +import CardBox from '../components/CardBox'; +import BaseButton from '../components/BaseButton'; +import BaseButtons from '../components/BaseButtons'; +import UserAvatarWithBadge from '../components/UserAvatarWithBadge'; + const Dashboard = () => { - const dispatch = useAppDispatch(); const iconsColor = useAppSelector((state) => state.style.iconsColor); - const corners = useAppSelector((state) => state.style.corners); - const cardsStyle = useAppSelector((state) => state.style.cardsStyle); - - const loadingMessage = 'Loading...'; - - - const [users, setUsers] = React.useState(loadingMessage); - const [roles, setRoles] = React.useState(loadingMessage); - const [permissions, setPermissions] = React.useState(loadingMessage); - const [provinces, setProvinces] = React.useState(loadingMessage); - const [districts, setDistricts] = React.useState(loadingMessage); - const [sectors, setSectors] = React.useState(loadingMessage); - const [cells, setCells] = React.useState(loadingMessage); - const [villages, setVillages] = React.useState(loadingMessage); - const [account_tiers, setAccount_tiers] = React.useState(loadingMessage); - const [subscriptions, setSubscriptions] = React.useState(loadingMessage); - const [wallet_transactions, setWallet_transactions] = React.useState(loadingMessage); - const [content_categories, setContent_categories] = React.useState(loadingMessage); - const [posts, setPosts] = React.useState(loadingMessage); - const [paid_actions, setPaid_actions] = React.useState(loadingMessage); - const [engagement_events, setEngagement_events] = React.useState(loadingMessage); - const [fee_rules, setFee_rules] = React.useState(loadingMessage); - const [rule_engine_events, setRule_engine_events] = React.useState(loadingMessage); - const [notifications, setNotifications] = React.useState(loadingMessage); - const [admin_settings, setAdmin_settings] = React.useState(loadingMessage); - const [help_center_articles, setHelp_center_articles] = React.useState(loadingMessage); - const [for_you_rankings, setFor_you_rankings] = React.useState(loadingMessage); - - - const [widgetsRole, setWidgetsRole] = React.useState({ - role: { value: '', label: '' }, - }); const { currentUser } = useAppSelector((state) => state.auth); - const { isFetchingQuery } = useAppSelector((state) => state.openAi); - - const { rolesWidgets, loading } = useAppSelector((state) => state.roles); - - - async function loadData() { - const entities = ['users','roles','permissions','provinces','districts','sectors','cells','villages','account_tiers','subscriptions','wallet_transactions','content_categories','posts','paid_actions','engagement_events','fee_rules','rule_engine_events','notifications','admin_settings','help_center_articles','for_you_rankings',]; - const fns = [setUsers,setRoles,setPermissions,setProvinces,setDistricts,setSectors,setCells,setVillages,setAccount_tiers,setSubscriptions,setWallet_transactions,setContent_categories,setPosts,setPaid_actions,setEngagement_events,setFee_rules,setRule_engine_events,setNotifications,setAdmin_settings,setHelp_center_articles,setFor_you_rankings,]; - const requests = entities.map((entity, index) => { - - if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) { - return axios.get(`/${entity.toLowerCase()}/count`); - } else { - fns[index](null); - return Promise.resolve({data: {count: null}}); - } - - }); + const [stats, setStats] = React.useState({ + posts: 0, + wallet_balance: 0, + paid_actions: 0, + notifications: 0 + }); - Promise.allSettled(requests).then((results) => { - results.forEach((result, i) => { - if (result.status === 'fulfilled') { - fns[i](result.value.data.count); - } else { - fns[i](result.reason.message); - } + async function loadStats() { + try { + const [postsCount, actionsCount, notifsCount] = await Promise.all([ + axios.get('/posts/count'), + axios.get('/paid_actions/count'), + axios.get('/notifications/count') + ]); + setStats({ + posts: postsCount.data.count, + wallet_balance: 0, // Placeholder as wallet logic is simulated + paid_actions: actionsCount.data.count, + notifications: notifsCount.data.count }); - }); - } - - async function getWidgets(roleId) { - await dispatch(fetchWidgets(roleId)); + } catch (e) { + console.error(e); + } } + React.useEffect(() => { - if (!currentUser) return; - loadData().then(); - setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } }); + if (currentUser) loadStats(); }, [currentUser]); - React.useEffect(() => { - if (!currentUser || !widgetsRole?.role?.value) return; - getWidgets(widgetsRole?.role?.value || '').then(); - }, [widgetsRole?.role?.value]); - - return ( - <> - - - {getPageTitle('Overview')} - - - - - {''} - - - {hasPermission(currentUser, 'CREATE_ROLES') && } - {!!rolesWidgets.length && - hasPermission(currentUser, 'CREATE_ROLES') && ( -

- {`${widgetsRole?.role?.label || 'Users'}'s widgets`} -

- )} - -
- {(isFetchingQuery || loading) && ( -
- {' '} - Loading widgets... -
- )} - - { rolesWidgets && - rolesWidgets.map((widget) => ( - + + {getPageTitle('Menyahafi Dashboard')} + + + + - ))} -
+ - {!!rolesWidgets.length &&
} - -
- - - {hasPermission(currentUser, 'READ_USERS') && -
-
-
-
- Users -
-
- {users} -
+ {/* User Status Card */} + +
+ +
+

{currentUser?.firstName}, Welcome back!

+

+ Current Tier: {currentUser?.account_tier?.name || 'Starter'} • + Location: {currentUser?.village?.name || 'Kigali'} +

-
- +
+

Wallet Balance

+

{stats.wallet_balance} RWF

+ + Deposit Funds + +
+
+ + +
+
+
+
+

My Posts

+

{stats.posts}

+
+ +
+
+
+
+
+

Paid Actions

+

{stats.paid_actions}

+
+ +
+
+
+
+
+

Alerts

+

{stats.notifications}

+
+ +
+
+
+
+
+

Trust Score

+

98%

+
+
- } - - {hasPermission(currentUser, 'READ_ROLES') && -
-
-
-
- Roles -
-
- {roles} -
-
-
- + +
+ + + +
-
+ + + +
+
+
+
+ AI Rules Engine +
+ ACTIVE +
+
+
+
+ Wallet Enforcement +
+ AUTO-SHIELD ON +
+
+
+
+ For You Algorithm +
+ V1.2 RUNNING +
+
+
- } - - {hasPermission(currentUser, 'READ_PERMISSIONS') && -
-
-
-
- Permissions -
-
- {permissions} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_PROVINCES') && -
-
-
-
- Provinces -
-
- {provinces} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_DISTRICTS') && -
-
-
-
- Districts -
-
- {districts} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_SECTORS') && -
-
-
-
- Sectors -
-
- {sectors} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_CELLS') && -
-
-
-
- Cells -
-
- {cells} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_VILLAGES') && -
-
-
-
- Villages -
-
- {villages} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_ACCOUNT_TIERS') && -
-
-
-
- Account tiers -
-
- {account_tiers} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_SUBSCRIPTIONS') && -
-
-
-
- Subscriptions -
-
- {subscriptions} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_WALLET_TRANSACTIONS') && -
-
-
-
- Wallet transactions -
-
- {wallet_transactions} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_CONTENT_CATEGORIES') && -
-
-
-
- Content categories -
-
- {content_categories} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_POSTS') && -
-
-
-
- Posts -
-
- {posts} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_PAID_ACTIONS') && -
-
-
-
- Paid actions -
-
- {paid_actions} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_ENGAGEMENT_EVENTS') && -
-
-
-
- Engagement events -
-
- {engagement_events} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_FEE_RULES') && -
-
-
-
- Fee rules -
-
- {fee_rules} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_RULE_ENGINE_EVENTS') && -
-
-
-
- Rule engine events -
-
- {rule_engine_events} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_NOTIFICATIONS') && -
-
-
-
- Notifications -
-
- {notifications} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_ADMIN_SETTINGS') && -
-
-
-
- Admin settings -
-
- {admin_settings} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_HELP_CENTER_ARTICLES') && -
-
-
-
- Help center articles -
-
- {help_center_articles} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_FOR_YOU_RANKINGS') && -
-
-
-
- For you rankings -
-
- {for_you_rankings} -
-
-
- -
-
-
- } - - -
- - - ) + + + ); } Dashboard.getLayout = function getLayout(page: ReactElement) { - return {page} + return {page} } -export default Dashboard +export default Dashboard; \ No newline at end of file diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 3aef749..7279791 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,166 +1,423 @@ - import React, { useEffect, useState } from 'react'; import type { ReactElement } from 'react'; import Head from 'next/head'; import Link from 'next/link'; -import BaseButton from '../components/BaseButton'; -import CardBox from '../components/CardBox'; -import SectionFullScreen from '../components/SectionFullScreen'; +import axios from 'axios'; +import SectionMain from '../components/SectionMain'; import LayoutGuest from '../layouts/Guest'; -import BaseDivider from '../components/BaseDivider'; -import BaseButtons from '../components/BaseButtons'; import { getPageTitle } from '../config'; import { useAppSelector } from '../stores/hooks'; -import CardBoxComponentTitle from "../components/CardBoxComponentTitle"; -import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'; +import { mdiWallet, mdiMapMarker, mdiStar, mdiBriefcase, mdiBullhorn, mdiChevronRight, mdiShieldCheck, mdiAlert } from '@mdi/js'; +import BaseIcon from '../components/BaseIcon'; +import WalletCard from '../components/WalletCard'; +import FormField from '../components/FormField'; +export default function MenyahafiLanding() { + const [posts, setPosts] = useState([]); + const [tiers, setTiers] = useState([]); + const [loading, setLoading] = useState(true); + const { currentUser } = useAppSelector((state) => state.auth); + + // Demo Post State + const [demoPostType, setDemoPostType] = useState('job_daily'); + const [demoTitle, setDemoTitle] = useState(''); + const [demoFee, setDemoFee] = useState(500); + const [userBalance, setUserBalance] = useState(0); -export default function Starter() { - const [illustrationImage, setIllustrationImage] = useState({ - src: undefined, - photographer: undefined, - photographer_url: undefined, - }) - const [illustrationVideo, setIllustrationVideo] = useState({video_files: []}) - const [contentType, setContentType] = useState('image'); - const [contentPosition, setContentPosition] = useState('right'); - const textColor = useAppSelector((state) => state.style.linkColor); - - const title = 'MENYAHAFI' - - // Fetch Pexels image/video useEffect(() => { - async function fetchData() { - const image = await getPexelsImage(); - const video = await getPexelsVideo(); - setIllustrationImage(image); - setIllustrationVideo(video); + const fetchLandingData = async () => { + try { + const response = await axios.get('/landing/data'); + setPosts(response.data.posts || []); + setTiers(response.data.tiers || []); + } catch (error) { + console.error('Failed to fetch landing data:', error); + } finally { + setLoading(false); + } + }; + fetchLandingData(); + + if (currentUser) { + axios.get('/wallet/balance').then(res => { + setUserBalance(Number(res.data.balance) || 0); + }); } - fetchData(); - }, []); + }, [currentUser]); - const imageBlock = (image) => ( -
-
- - Photo by {image?.photographer} on Pexels - + useEffect(() => { + // Simple fee logic for demo + let baseFee = 500; + if (demoPostType === 'job_monthly') baseFee = 2000; + if (demoPostType.startsWith('ad_')) baseFee = 3500; + if (demoPostType === 'nyakabyizi') baseFee = 0; + + // AI detection of links (mock) + const hasLinks = /http|www|\.com|\.rw|07[8923]\d{7}/.test(demoTitle); + if (hasLinks) baseFee += 1500; + + setDemoFee(baseFee); + }, [demoPostType, demoTitle]); + + const defaultTiers = [ + { name: 'Starter', price: '0', color: 'bg-gray-100', text: 'text-gray-800' }, + { name: 'Bronze', price: '1,000', color: 'bg-orange-100', text: 'text-orange-800' }, + { name: 'Silver', price: '3,000', color: 'bg-slate-200', text: 'text-slate-800' }, + { name: 'Gold', price: '6,000', color: 'bg-yellow-100', text: 'text-yellow-800' }, + { name: 'Platinum', price: '10,000', color: 'bg-blue-100', text: 'text-blue-800' }, + { name: 'Plutonium', price: '15,000', color: 'bg-purple-100', text: 'text-purple-800' }, + ]; + + const displayTiers = tiers.length > 0 ? tiers.map((t: any) => ({ + name: t.name, + price: t.subscription_fee_rwf, + color: t.name.toLowerCase().includes('platinum') ? 'bg-blue-100' : (t.name.toLowerCase().includes('gold') ? 'bg-yellow-100' : 'bg-gray-100'), + text: 'text-gray-800' + })) : defaultTiers; + + const isBalanceInsufficient = currentUser && userBalance < demoFee; + + return ( +
+ + {getPageTitle('MENYAHAFI - Paid Digital Marketplace Rwanda')} + + + {/* Hero Section */} +
+
+
+
+
+ +
+
+ + 100% Automated Trusted Platform +
+

+ MENYAHAFI +

+

+ The heartbeat of Rwanda's digital economy. + Safe jobs, professional ads, and verified local engagement. +

+
+ {!currentUser ? ( + <> + + + + + + + + ) : ( + + + + )} +
+
+ + + {/* Stats / Wallet Promo */} +
+ {currentUser ? ( + + ) : ( +
+
+
+ +
+
+

Your Wallet

+

Login to View

+
+
+
+ )} + +
+
+
+ +
+
+

Jobs Posted

+

2,450+

+
+
+
+ +
+
+
+ +
+
+

Verified Ads

+

1,200+

+
+
+
+
+ + {/* AI Fee Engine Demo */} +
+
+
+
+ + Automatic Fee Engine +
+

+ Test Our AI-Powered
Pricing Real-Time +

+

+ See how our rules engine calculates fees based on category, + content detection (links/scams), and your location. +

+ +
+ + + + + +