Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7560ce595 |
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
@ -32,6 +31,11 @@ module.exports = class UsersDBApi {
|
|||||||
||
|
||
|
||||||
null
|
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
|
phoneNumber: data.data.phoneNumber
|
||||||
||
|
||
|
||||||
@ -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 || [], {
|
await users.setCustom_permissions(data.data.custom_permissions || [], {
|
||||||
@ -262,43 +271,27 @@ module.exports = class UsersDBApi {
|
|||||||
const updatePayload = {};
|
const updatePayload = {};
|
||||||
|
|
||||||
if (data.firstName !== undefined) updatePayload.firstName = data.firstName;
|
if (data.firstName !== undefined) updatePayload.firstName = data.firstName;
|
||||||
|
|
||||||
|
|
||||||
if (data.lastName !== undefined) updatePayload.lastName = data.lastName;
|
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.phoneNumber !== undefined) updatePayload.phoneNumber = data.phoneNumber;
|
||||||
|
|
||||||
|
|
||||||
if (data.email !== undefined) updatePayload.email = data.email;
|
if (data.email !== undefined) updatePayload.email = data.email;
|
||||||
|
|
||||||
|
|
||||||
if (data.disabled !== undefined) updatePayload.disabled = data.disabled;
|
if (data.disabled !== undefined) updatePayload.disabled = data.disabled;
|
||||||
|
|
||||||
|
|
||||||
if (data.password !== undefined) updatePayload.password = data.password;
|
if (data.password !== undefined) updatePayload.password = data.password;
|
||||||
|
|
||||||
|
|
||||||
if (data.emailVerified !== undefined) updatePayload.emailVerified = data.emailVerified;
|
if (data.emailVerified !== undefined) updatePayload.emailVerified = data.emailVerified;
|
||||||
|
|
||||||
else updatePayload.emailVerified = true;
|
else updatePayload.emailVerified = true;
|
||||||
|
|
||||||
|
|
||||||
if (data.emailVerificationToken !== undefined) updatePayload.emailVerificationToken = data.emailVerificationToken;
|
if (data.emailVerificationToken !== undefined) updatePayload.emailVerificationToken = data.emailVerificationToken;
|
||||||
|
|
||||||
|
|
||||||
if (data.emailVerificationTokenExpiresAt !== undefined) updatePayload.emailVerificationTokenExpiresAt = data.emailVerificationTokenExpiresAt;
|
if (data.emailVerificationTokenExpiresAt !== undefined) updatePayload.emailVerificationTokenExpiresAt = data.emailVerificationTokenExpiresAt;
|
||||||
|
|
||||||
|
|
||||||
if (data.passwordResetToken !== undefined) updatePayload.passwordResetToken = data.passwordResetToken;
|
if (data.passwordResetToken !== undefined) updatePayload.passwordResetToken = data.passwordResetToken;
|
||||||
|
|
||||||
|
|
||||||
if (data.passwordResetTokenExpiresAt !== undefined) updatePayload.passwordResetTokenExpiresAt = data.passwordResetTokenExpiresAt;
|
if (data.passwordResetTokenExpiresAt !== undefined) updatePayload.passwordResetTokenExpiresAt = data.passwordResetTokenExpiresAt;
|
||||||
|
|
||||||
|
|
||||||
if (data.provider !== undefined) updatePayload.provider = data.provider;
|
if (data.provider !== undefined) updatePayload.provider = data.provider;
|
||||||
|
|
||||||
|
|
||||||
updatePayload.updatedById = currentUser.id;
|
updatePayload.updatedById = currentUser.id;
|
||||||
|
|
||||||
await users.update(updatePayload, {transaction});
|
await users.update(updatePayload, {transaction});
|
||||||
@ -314,6 +307,13 @@ module.exports = class UsersDBApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
static async remove(id, options) {
|
||||||
@ -463,6 +463,13 @@ module.exports = class UsersDBApi {
|
|||||||
transaction
|
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) {
|
if (output.app_role) {
|
||||||
output.app_role_permissions = await output.app_role.getPermissions({
|
output.app_role_permissions = await output.app_role.getPermissions({
|
||||||
transaction,
|
transaction,
|
||||||
@ -974,4 +981,3 @@ module.exports = class UsersDBApi {
|
|||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
82
backend/src/db/migrations/1770756403378.js
Normal file
82
backend/src/db/migrations/1770756403378.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -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: {
|
phoneNumber: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
@ -104,6 +125,12 @@ provider: {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
wallet_balance_rwf: {
|
||||||
|
type: DataTypes.DECIMAL,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
|
||||||
importHash: {
|
importHash: {
|
||||||
type: DataTypes.STRING(255),
|
type: DataTypes.STRING(255),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
@ -239,6 +266,53 @@ provider: {
|
|||||||
constraints: false,
|
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, {
|
db.users.hasMany(db.file, {
|
||||||
@ -305,4 +379,3 @@ function trimStringFields(users) {
|
|||||||
|
|
||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const app = express();
|
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 for_you_rankingsRoutes = require('./routes/for_you_rankings');
|
||||||
|
|
||||||
|
const walletRoutes = require('./routes/wallet');
|
||||||
|
|
||||||
const getBaseUrl = (url) => {
|
const getBaseUrl = (url) => {
|
||||||
if (!url) return '';
|
if (!url) return '';
|
||||||
@ -118,6 +118,7 @@ app.use(bodyParser.json());
|
|||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api/file', fileRoutes);
|
app.use('/api/file', fileRoutes);
|
||||||
app.use('/api/pexels', pexelsRoutes);
|
app.use('/api/pexels', pexelsRoutes);
|
||||||
|
app.use('/api/landing', require('./routes/landing'));
|
||||||
app.enable('trust proxy');
|
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/for_you_rankings', passport.authenticate('jwt', {session: false}), for_you_rankingsRoutes);
|
||||||
|
|
||||||
|
app.use('/api/wallet', walletRoutes);
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
'/api/openai',
|
'/api/openai',
|
||||||
passport.authenticate('jwt', { session: false }),
|
passport.authenticate('jwt', { session: false }),
|
||||||
|
|||||||
21
backend/src/routes/landing.js
Normal file
21
backend/src/routes/landing.js
Normal file
@ -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;
|
||||||
66
backend/src/routes/wallet.js
Normal file
66
backend/src/routes/wallet.js
Normal file
@ -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;
|
||||||
@ -1,11 +1,12 @@
|
|||||||
import React, { ReactNode, useState, useEffect } from 'react'
|
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 { containerMaxW } from '../config'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
import NavBarItemPlain from './NavBarItemPlain'
|
import NavBarItemPlain from './NavBarItemPlain'
|
||||||
import NavBarMenuList from './NavBarMenuList'
|
import NavBarMenuList from './NavBarMenuList'
|
||||||
import { MenuNavBarItem } from '../interfaces'
|
import { MenuNavBarItem } from '../interfaces'
|
||||||
import { useAppSelector } from '../stores/hooks';
|
import { useAppSelector } from '../stores/hooks';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
menu: MenuNavBarItem[]
|
menu: MenuNavBarItem[]
|
||||||
@ -17,6 +18,7 @@ export default function NavBar({ menu, className = '', children }: Props) {
|
|||||||
const [isMenuNavBarActive, setIsMenuNavBarActive] = useState(false)
|
const [isMenuNavBarActive, setIsMenuNavBarActive] = useState(false)
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
||||||
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
@ -39,6 +41,16 @@ export default function NavBar({ menu, className = '', children }: Props) {
|
|||||||
>
|
>
|
||||||
<div className={`flex lg:items-stretch ${containerMaxW} ${isScrolled && `border-b border-pavitra-400 dark:border-dark-700`}`}>
|
<div className={`flex lg:items-stretch ${containerMaxW} ${isScrolled && `border-b border-pavitra-400 dark:border-dark-700`}`}>
|
||||||
<div className="flex flex-1 items-stretch h-14">{children}</div>
|
<div className="flex flex-1 items-stretch h-14">{children}</div>
|
||||||
|
|
||||||
|
{currentUser && (
|
||||||
|
<div className="flex items-center px-4">
|
||||||
|
<Link href="/wallet_transactions/wallet_transactions-list" className="flex items-center gap-2 bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-100 px-3 py-1 rounded-full text-sm font-bold border border-blue-200 dark:border-blue-800 transition-transform hover:scale-105">
|
||||||
|
<BaseIcon path={mdiWallet} size={18} />
|
||||||
|
<span>0 RWF</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex-none items-stretch flex h-14 lg:hidden">
|
<div className="flex-none items-stretch flex h-14 lg:hidden">
|
||||||
<NavBarItemPlain onClick={handleMenuNavBarToggleClick}>
|
<NavBarItemPlain onClick={handleMenuNavBarToggleClick}>
|
||||||
<BaseIcon path={isMenuNavBarActive ? mdiClose : mdiDotsVertical} size="24" />
|
<BaseIcon path={isMenuNavBarActive ? mdiClose : mdiDotsVertical} size="24" />
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import React, {useEffect, useRef} from 'react'
|
import React, {useEffect, useRef, useState} from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useState } from 'react'
|
|
||||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||||
import BaseDivider from './BaseDivider'
|
import BaseDivider from './BaseDivider'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
|
|||||||
48
frontend/src/components/UserAvatarWithBadge.tsx
Normal file
48
frontend/src/components/UserAvatarWithBadge.tsx
Normal file
@ -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 (
|
||||||
|
<div className={`relative inline-block ${className}`} style={{ width: size, height: size }}>
|
||||||
|
{avatarUrl ? (
|
||||||
|
<img
|
||||||
|
src={avatarUrl}
|
||||||
|
alt={user?.firstName || 'User'}
|
||||||
|
className="rounded-full w-full h-full object-cover border-2 border-white shadow-sm"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="bg-gray-200 rounded-full w-full h-full flex items-center justify-center border-2 border-white shadow-sm">
|
||||||
|
<BaseIcon path={mdiAccount} size={size * 0.6} className="text-gray-400" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{badgeColor && (
|
||||||
|
<div className={`absolute -bottom-1 -right-1 ${badgeColor} text-white text-[10px] font-bold px-1.5 py-0.5 rounded-full border-2 border-white shadow-sm uppercase tracking-tighter`}>
|
||||||
|
{user.account_tier.name.substring(0, 1)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
95
frontend/src/components/WalletCard.tsx
Normal file
95
frontend/src/components/WalletCard.tsx
Normal file
@ -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<number>(0);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [depositAmount, setDepositAmount] = useState<string>('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 (
|
||||||
|
<>
|
||||||
|
<CardBoxModal
|
||||||
|
title="Simulate Deposit"
|
||||||
|
buttonColor="info"
|
||||||
|
buttonLabel="Deposit Now"
|
||||||
|
isActive={isModalOpen}
|
||||||
|
onConfirm={handleDeposit}
|
||||||
|
onCancel={() => setIsModalOpen(false)}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
<p className="mb-4 text-gray-600">
|
||||||
|
This is a <b>Wallet Simulation</b>. No real money will be charged.
|
||||||
|
Enter the amount in RWF you want to add to your MENYAHAFI account.
|
||||||
|
</p>
|
||||||
|
<FormField label="Amount (RWF)" help="Min 100 RWF">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={depositAmount}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</CardBoxModal>
|
||||||
|
|
||||||
|
<div className="bg-white p-6 rounded-2xl shadow-xl border-b-4 border-blue-500">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="p-3 bg-blue-100 rounded-lg text-blue-600">
|
||||||
|
<BaseIcon path={mdiWallet} size={32} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500 font-bold uppercase tracking-wider">Your Balance</p>
|
||||||
|
<p className="text-2xl font-black text-gray-900">
|
||||||
|
{balance.toLocaleString()} RWF
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
className="p-2 hover:bg-blue-50 text-blue-600 rounded-full transition-colors flex items-center gap-1 group"
|
||||||
|
title="Add Funds"
|
||||||
|
>
|
||||||
|
<BaseIcon path={mdiPlusCircle} size={24} />
|
||||||
|
<span className="text-sm font-bold hidden group-hover:inline">Deposit</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WalletCard;
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import React, { ReactNode, useEffect } from 'react'
|
import React, { ReactNode, useEffect, useState } from 'react'
|
||||||
import { useState } from 'react'
|
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||||
import menuAside from '../menuAside'
|
import menuAside from '../menuAside'
|
||||||
|
|||||||
@ -11,748 +11,195 @@ import { getPageTitle } from '../config'
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { hasPermission } from "../helpers/userPermissions";
|
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 { 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 Dashboard = () => {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
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 { currentUser } = useAppSelector((state) => state.auth);
|
||||||
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
|
|
||||||
|
|
||||||
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
|
const [stats, setStats] = React.useState({
|
||||||
|
posts: 0,
|
||||||
|
wallet_balance: 0,
|
||||||
|
paid_actions: 0,
|
||||||
|
notifications: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadStats() {
|
||||||
async function loadData() {
|
try {
|
||||||
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 [postsCount, actionsCount, notifsCount] = await Promise.all([
|
||||||
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,];
|
axios.get('/posts/count'),
|
||||||
|
axios.get('/paid_actions/count'),
|
||||||
const requests = entities.map((entity, index) => {
|
axios.get('/notifications/count')
|
||||||
|
]);
|
||||||
if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
|
setStats({
|
||||||
return axios.get(`/${entity.toLowerCase()}/count`);
|
posts: postsCount.data.count,
|
||||||
} else {
|
wallet_balance: 0, // Placeholder as wallet logic is simulated
|
||||||
fns[index](null);
|
paid_actions: actionsCount.data.count,
|
||||||
return Promise.resolve({data: {count: null}});
|
notifications: notifsCount.data.count
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getWidgets(roleId) {
|
|
||||||
await dispatch(fetchWidgets(roleId));
|
|
||||||
}
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!currentUser) return;
|
if (currentUser) loadStats();
|
||||||
loadData().then();
|
|
||||||
setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } });
|
|
||||||
}, [currentUser]);
|
}, [currentUser]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
return (
|
||||||
if (!currentUser || !widgetsRole?.role?.value) return;
|
<>
|
||||||
getWidgets(widgetsRole?.role?.value || '').then();
|
<Head>
|
||||||
}, [widgetsRole?.role?.value]);
|
<title>{getPageTitle('Menyahafi Dashboard')}</title>
|
||||||
|
</Head>
|
||||||
return (
|
<SectionMain>
|
||||||
<>
|
<SectionTitleLineWithButton
|
||||||
<Head>
|
icon={icon.mdiViewDashboard}
|
||||||
<title>
|
title='Menyahafi Dashboard'
|
||||||
{getPageTitle('Overview')}
|
main
|
||||||
</title>
|
>
|
||||||
</Head>
|
<BaseButton
|
||||||
<SectionMain>
|
href="/posts/posts-new"
|
||||||
<SectionTitleLineWithButton
|
label="New Post"
|
||||||
icon={icon.mdiChartTimelineVariant}
|
color="info"
|
||||||
title='Overview'
|
icon={icon.mdiPlus}
|
||||||
main>
|
|
||||||
{''}
|
|
||||||
</SectionTitleLineWithButton>
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
|
|
||||||
currentUser={currentUser}
|
|
||||||
isFetchingQuery={isFetchingQuery}
|
|
||||||
setWidgetsRole={setWidgetsRole}
|
|
||||||
widgetsRole={widgetsRole}
|
|
||||||
/>}
|
|
||||||
{!!rolesWidgets.length &&
|
|
||||||
hasPermission(currentUser, 'CREATE_ROLES') && (
|
|
||||||
<p className=' text-gray-500 dark:text-gray-400 mb-4'>
|
|
||||||
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'>
|
|
||||||
{(isFetchingQuery || loading) && (
|
|
||||||
<div className={` ${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 text-lg leading-tight text-gray-500 flex items-center ${cardsStyle} dark:border-dark-700 p-6`}>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor} animate-spin mr-5`}
|
|
||||||
w='w-16'
|
|
||||||
h='h-16'
|
|
||||||
size={48}
|
|
||||||
path={icon.mdiLoading}
|
|
||||||
/>{' '}
|
|
||||||
Loading widgets...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{ rolesWidgets &&
|
|
||||||
rolesWidgets.map((widget) => (
|
|
||||||
<SmartWidget
|
|
||||||
key={widget.id}
|
|
||||||
userId={currentUser?.id}
|
|
||||||
widget={widget}
|
|
||||||
roleId={widgetsRole?.role?.value || ''}
|
|
||||||
admin={hasPermission(currentUser, 'CREATE_ROLES')}
|
|
||||||
/>
|
/>
|
||||||
))}
|
</SectionTitleLineWithButton>
|
||||||
</div>
|
|
||||||
|
|
||||||
{!!rolesWidgets.length && <hr className='my-6 ' />}
|
{/* User Status Card */}
|
||||||
|
<CardBox className="mb-6 border-l-8 border-blue-600">
|
||||||
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
|
<div className="flex flex-col md:flex-row items-center gap-6">
|
||||||
|
<UserAvatarWithBadge user={currentUser} size={80} />
|
||||||
|
<div className="flex-grow text-center md:text-left">
|
||||||
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}>
|
<h2 className="text-2xl font-bold">{currentUser?.firstName}, Welcome back!</h2>
|
||||||
<div
|
<p className="text-gray-500">
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
Current Tier: <span className="font-bold text-blue-600">{currentUser?.account_tier?.name || 'Starter'}</span> •
|
||||||
>
|
Location: <span className="font-bold">{currentUser?.village?.name || 'Kigali'}</span>
|
||||||
<div className="flex justify-between align-center">
|
</p>
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Users
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{users}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="bg-blue-50 p-4 rounded-2xl text-center min-w-[150px]">
|
||||||
<BaseIcon
|
<p className="text-xs text-blue-600 font-bold uppercase tracking-wider">Wallet Balance</p>
|
||||||
className={`${iconsColor}`}
|
<p className="text-2xl font-black text-blue-900">{stats.wallet_balance} RWF</p>
|
||||||
w="w-16"
|
<Link href="/wallet_transactions/wallet_transactions-list" className="text-xs text-blue-600 font-bold hover:underline mt-1 block">
|
||||||
h="h-16"
|
Deposit Funds
|
||||||
size={48}
|
</Link>
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
</div>
|
||||||
// @ts-ignore
|
</div>
|
||||||
path={icon.mdiAccountGroup || icon.mdiTable}
|
</CardBox>
|
||||||
/>
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
|
||||||
|
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500 font-bold uppercase">My Posts</p>
|
||||||
|
<p className="text-3xl font-black">{stats.posts}</p>
|
||||||
|
</div>
|
||||||
|
<BaseIcon path={icon.mdiPostOutline} size={32} className="text-blue-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500 font-bold uppercase">Paid Actions</p>
|
||||||
|
<p className="text-3xl font-black">{stats.paid_actions}</p>
|
||||||
|
</div>
|
||||||
|
<BaseIcon path={icon.mdiCashCheck} size={32} className="text-green-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500 font-bold uppercase">Alerts</p>
|
||||||
|
<p className="text-3xl font-black">{stats.notifications}</p>
|
||||||
|
</div>
|
||||||
|
<BaseIcon path={icon.mdiBellAlertOutline} size={32} className="text-red-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500 font-bold uppercase">Trust Score</p>
|
||||||
|
<p className="text-3xl font-black">98%</p>
|
||||||
|
</div>
|
||||||
|
<BaseIcon path={icon.mdiShieldCheckOutline} size={32} className="text-purple-500" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_ROLES') && <Link href={'/roles/roles-list'}>
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<div
|
<CardBox title="Quick Actions" icon={icon.mdiFlash}>
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
<div className="grid grid-cols-2 gap-4">
|
||||||
>
|
<BaseButton
|
||||||
<div className="flex justify-between align-center">
|
href="/posts/posts-new"
|
||||||
<div>
|
label="Post a Job"
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
color="info"
|
||||||
Roles
|
outline
|
||||||
</div>
|
className="w-full"
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
icon={icon.mdiBriefcasePlus}
|
||||||
{roles}
|
/>
|
||||||
</div>
|
<BaseButton
|
||||||
</div>
|
href="/posts/posts-new"
|
||||||
<div>
|
label="Post an Ad"
|
||||||
<BaseIcon
|
color="success"
|
||||||
className={`${iconsColor}`}
|
outline
|
||||||
w="w-16"
|
className="w-full"
|
||||||
h="h-16"
|
icon={icon.mdiBullhornVariant}
|
||||||
size={48}
|
/>
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
<BaseButton
|
||||||
// @ts-ignore
|
href="/account_tiers/account_tiers-list"
|
||||||
path={icon.mdiShieldAccountVariantOutline || icon.mdiTable}
|
label="Upgrade Tier"
|
||||||
|
color="warning"
|
||||||
|
outline
|
||||||
|
className="w-full"
|
||||||
|
icon={icon.mdiShieldCrown}
|
||||||
|
/>
|
||||||
|
<BaseButton
|
||||||
|
href="/help_center_articles/help_center_articles-list"
|
||||||
|
label="Help Center"
|
||||||
|
color="contrast"
|
||||||
|
outline
|
||||||
|
className="w-full"
|
||||||
|
icon={icon.mdiHelpCircle}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardBox>
|
||||||
|
|
||||||
|
<CardBox title="System Status" icon={icon.mdiRobot}>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-xl">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
|
||||||
|
<span className="font-bold text-sm">AI Rules Engine</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-500 font-bold">ACTIVE</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-xl">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
|
||||||
|
<span className="font-bold text-sm">Wallet Enforcement</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-500 font-bold">AUTO-SHIELD ON</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-xl">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
|
||||||
|
<span className="font-bold text-sm">For You Algorithm</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-500 font-bold">V1.2 RUNNING</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
</div>
|
</div>
|
||||||
</Link>}
|
</SectionMain>
|
||||||
|
</>
|
||||||
{hasPermission(currentUser, 'READ_PERMISSIONS') && <Link href={'/permissions/permissions-list'}>
|
);
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Permissions
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{permissions}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={icon.mdiShieldAccountOutline || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_PROVINCES') && <Link href={'/provinces/provinces-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Provinces
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{provinces}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiMap' in icon ? icon['mdiMap' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_DISTRICTS') && <Link href={'/districts/districts-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Districts
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{districts}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiMapMarker' in icon ? icon['mdiMapMarker' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_SECTORS') && <Link href={'/sectors/sectors-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Sectors
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{sectors}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiMapMarkerRadius' in icon ? icon['mdiMapMarkerRadius' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_CELLS') && <Link href={'/cells/cells-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Cells
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{cells}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiMapMarkerOutline' in icon ? icon['mdiMapMarkerOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_VILLAGES') && <Link href={'/villages/villages-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Villages
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{villages}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiHomeMapMarker' in icon ? icon['mdiHomeMapMarker' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_ACCOUNT_TIERS') && <Link href={'/account_tiers/account_tiers-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Account tiers
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{account_tiers}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiShieldCrown' in icon ? icon['mdiShieldCrown' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_SUBSCRIPTIONS') && <Link href={'/subscriptions/subscriptions-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Subscriptions
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{subscriptions}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiCalendarClock' in icon ? icon['mdiCalendarClock' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_WALLET_TRANSACTIONS') && <Link href={'/wallet_transactions/wallet_transactions-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Wallet transactions
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{wallet_transactions}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiCashSync' in icon ? icon['mdiCashSync' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_CONTENT_CATEGORIES') && <Link href={'/content_categories/content_categories-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Content categories
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{content_categories}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiTagMultiple' in icon ? icon['mdiTagMultiple' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_POSTS') && <Link href={'/posts/posts-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Posts
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{posts}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiPostOutline' in icon ? icon['mdiPostOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_PAID_ACTIONS') && <Link href={'/paid_actions/paid_actions-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Paid actions
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{paid_actions}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiLockCheck' in icon ? icon['mdiLockCheck' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_ENGAGEMENT_EVENTS') && <Link href={'/engagement_events/engagement_events-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Engagement events
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{engagement_events}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiThumbUpOutline' in icon ? icon['mdiThumbUpOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_FEE_RULES') && <Link href={'/fee_rules/fee_rules-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Fee rules
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{fee_rules}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiCalculatorVariant' in icon ? icon['mdiCalculatorVariant' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_RULE_ENGINE_EVENTS') && <Link href={'/rule_engine_events/rule_engine_events-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Rule engine events
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{rule_engine_events}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiRobotAlert' in icon ? icon['mdiRobotAlert' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_NOTIFICATIONS') && <Link href={'/notifications/notifications-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Notifications
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{notifications}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiBellOutline' in icon ? icon['mdiBellOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_ADMIN_SETTINGS') && <Link href={'/admin_settings/admin_settings-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Admin settings
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{admin_settings}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiCogOutline' in icon ? icon['mdiCogOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_HELP_CENTER_ARTICLES') && <Link href={'/help_center_articles/help_center_articles-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Help center articles
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{help_center_articles}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiHelpCircleOutline' in icon ? icon['mdiHelpCircleOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_FOR_YOU_RANKINGS') && <Link href={'/for_you_rankings/for_you_rankings-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
For you rankings
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{for_you_rankings}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiChartLine' in icon ? icon['mdiChartLine' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</SectionMain>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Dashboard.getLayout = function getLayout(page: ReactElement) {
|
Dashboard.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Dashboard
|
export default Dashboard;
|
||||||
@ -1,166 +1,423 @@
|
|||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import BaseButton from '../components/BaseButton';
|
import axios from 'axios';
|
||||||
import CardBox from '../components/CardBox';
|
import SectionMain from '../components/SectionMain';
|
||||||
import SectionFullScreen from '../components/SectionFullScreen';
|
|
||||||
import LayoutGuest from '../layouts/Guest';
|
import LayoutGuest from '../layouts/Guest';
|
||||||
import BaseDivider from '../components/BaseDivider';
|
|
||||||
import BaseButtons from '../components/BaseButtons';
|
|
||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
import { useAppSelector } from '../stores/hooks';
|
import { useAppSelector } from '../stores/hooks';
|
||||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
import { mdiWallet, mdiMapMarker, mdiStar, mdiBriefcase, mdiBullhorn, mdiChevronRight, mdiShieldCheck, mdiAlert } from '@mdi/js';
|
||||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
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);
|
||||||
|
|
||||||
export default function Starter() {
|
// Demo Post State
|
||||||
const [illustrationImage, setIllustrationImage] = useState({
|
const [demoPostType, setDemoPostType] = useState('job_daily');
|
||||||
src: undefined,
|
const [demoTitle, setDemoTitle] = useState('');
|
||||||
photographer: undefined,
|
const [demoFee, setDemoFee] = useState(500);
|
||||||
photographer_url: undefined,
|
const [userBalance, setUserBalance] = useState(0);
|
||||||
})
|
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
async function fetchData() {
|
const fetchLandingData = async () => {
|
||||||
const image = await getPexelsImage();
|
try {
|
||||||
const video = await getPexelsVideo();
|
const response = await axios.get('/landing/data');
|
||||||
setIllustrationImage(image);
|
setPosts(response.data.posts || []);
|
||||||
setIllustrationVideo(video);
|
setTiers(response.data.tiers || []);
|
||||||
}
|
} catch (error) {
|
||||||
fetchData();
|
console.error('Failed to fetch landing data:', error);
|
||||||
}, []);
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchLandingData();
|
||||||
|
|
||||||
const imageBlock = (image) => (
|
if (currentUser) {
|
||||||
<div
|
axios.get('/wallet/balance').then(res => {
|
||||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
setUserBalance(Number(res.data.balance) || 0);
|
||||||
style={{
|
});
|
||||||
backgroundImage: `${
|
}
|
||||||
image
|
}, [currentUser]);
|
||||||
? `url(${image?.src?.original})`
|
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
useEffect(() => {
|
||||||
}`,
|
// Simple fee logic for demo
|
||||||
backgroundSize: 'cover',
|
let baseFee = 500;
|
||||||
backgroundPosition: 'left center',
|
if (demoPostType === 'job_monthly') baseFee = 2000;
|
||||||
backgroundRepeat: 'no-repeat',
|
if (demoPostType.startsWith('ad_')) baseFee = 3500;
|
||||||
}}
|
if (demoPostType === 'nyakabyizi') baseFee = 0;
|
||||||
>
|
|
||||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
// AI detection of links (mock)
|
||||||
<a
|
const hasLinks = /http|www|\.com|\.rw|07[8923]\d{7}/.test(demoTitle);
|
||||||
className='text-[8px]'
|
if (hasLinks) baseFee += 1500;
|
||||||
href={image?.photographer_url}
|
|
||||||
target='_blank'
|
setDemoFee(baseFee);
|
||||||
rel='noreferrer'
|
}, [demoPostType, demoTitle]);
|
||||||
>
|
|
||||||
Photo by {image?.photographer} on Pexels
|
const defaultTiers = [
|
||||||
</a>
|
{ 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 (
|
||||||
|
<div className="bg-gray-50 min-h-screen font-sans">
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('MENYAHAFI - Paid Digital Marketplace Rwanda')}</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="bg-gradient-to-r from-blue-700 via-green-600 to-green-700 text-white py-24 px-4 overflow-hidden relative">
|
||||||
|
<div className="absolute top-0 left-0 w-full h-full opacity-10 pointer-events-none">
|
||||||
|
<div className="absolute -top-24 -left-24 w-96 h-96 bg-white rounded-full blur-3xl"></div>
|
||||||
|
<div className="absolute -bottom-24 -right-24 w-96 h-96 bg-yellow-400 rounded-full blur-3xl"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-6xl mx-auto text-center relative z-10">
|
||||||
|
<div className="inline-flex items-center gap-2 bg-white/10 backdrop-blur-md px-4 py-2 rounded-full mb-8 border border-white/20">
|
||||||
|
<BaseIcon path={mdiShieldCheck} size={20} className="text-yellow-400" />
|
||||||
|
<span className="text-sm font-bold uppercase tracking-widest">100% Automated Trusted Platform</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-6xl md:text-8xl font-black mb-8 tracking-tighter leading-none">
|
||||||
|
MENYA<span className="text-yellow-400">HAFI</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl md:text-2xl mb-12 font-medium max-w-3xl mx-auto text-blue-50 leading-relaxed">
|
||||||
|
The heartbeat of Rwanda's digital economy.
|
||||||
|
Safe jobs, professional ads, and verified local engagement.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row justify-center gap-6">
|
||||||
|
{!currentUser ? (
|
||||||
|
<>
|
||||||
|
<Link href="/register">
|
||||||
|
<button className="bg-yellow-400 hover:bg-yellow-500 text-blue-900 px-10 py-5 rounded-2xl font-black text-xl transition-all hover:scale-105 shadow-[0_10px_20px_rgba(250,210,1,0.3)]">
|
||||||
|
Join Menyahafi
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/login">
|
||||||
|
<button className="bg-white/10 hover:bg-white/20 text-white border-2 border-white/30 backdrop-blur-sm px-10 py-5 rounded-2xl font-black text-xl transition-all shadow-xl">
|
||||||
|
Sign In
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Link href="/dashboard">
|
||||||
|
<button className="bg-white text-blue-700 px-10 py-5 rounded-2xl font-black text-xl transition-all hover:scale-105 shadow-2xl">
|
||||||
|
Go to Dashboard
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SectionMain>
|
||||||
|
{/* Stats / Wallet Promo */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 -mt-16 mb-20">
|
||||||
|
{currentUser ? (
|
||||||
|
<WalletCard />
|
||||||
|
) : (
|
||||||
|
<div className="bg-white p-8 rounded-3xl shadow-2xl border-b-8 border-blue-500 transform hover:-translate-y-1 transition-transform">
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<div className="p-4 bg-blue-50 rounded-2xl text-blue-600">
|
||||||
|
<BaseIcon path={mdiWallet} size={40} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-400 font-black uppercase tracking-widest mb-1">Your Wallet</p>
|
||||||
|
<p className="text-3xl font-black text-gray-900 italic tracking-tighter">Login to View</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="bg-white p-8 rounded-3xl shadow-2xl border-b-8 border-green-500 transform hover:-translate-y-1 transition-transform">
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<div className="p-4 bg-green-50 rounded-2xl text-green-600">
|
||||||
|
<BaseIcon path={mdiBriefcase} size={40} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-400 font-black uppercase tracking-widest mb-1">Jobs Posted</p>
|
||||||
|
<p className="text-3xl font-black text-gray-900 tracking-tighter">2,450+</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white p-8 rounded-3xl shadow-2xl border-b-8 border-yellow-500 transform hover:-translate-y-1 transition-transform">
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<div className="p-4 bg-yellow-50 rounded-2xl text-yellow-600">
|
||||||
|
<BaseIcon path={mdiBullhorn} size={40} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-400 font-black uppercase tracking-widest mb-1">Verified Ads</p>
|
||||||
|
<p className="text-3xl font-black text-gray-900 tracking-tighter">1,200+</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* AI Fee Engine Demo */}
|
||||||
|
<div className="bg-white rounded-[2rem] shadow-2xl border border-gray-100 overflow-hidden mb-20">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2">
|
||||||
|
<div className="p-10 lg:p-16 bg-gray-50 border-r border-gray-100">
|
||||||
|
<div className="inline-flex items-center gap-2 bg-blue-100 text-blue-700 px-4 py-1 rounded-full text-xs font-black uppercase mb-6">
|
||||||
|
<BaseIcon path={mdiStar} size={14} />
|
||||||
|
Automatic Fee Engine
|
||||||
|
</div>
|
||||||
|
<h2 className="text-4xl font-black text-gray-900 mb-6 leading-tight">
|
||||||
|
Test Our AI-Powered <br/> Pricing Real-Time
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-500 mb-10 text-lg font-medium leading-relaxed">
|
||||||
|
See how our rules engine calculates fees based on category,
|
||||||
|
content detection (links/scams), and your location.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<FormField label="What are you posting?">
|
||||||
|
<select
|
||||||
|
value={demoPostType}
|
||||||
|
onChange={(e) => setDemoPostType(e.target.value)}
|
||||||
|
className="w-full bg-white border-2 border-gray-200 rounded-2xl px-5 py-4 font-bold text-gray-700 focus:border-blue-500 focus:ring-0 transition-all outline-none"
|
||||||
|
>
|
||||||
|
<option value="job_daily">Daily Job Post</option>
|
||||||
|
<option value="job_monthly">Monthly Job Post</option>
|
||||||
|
<option value="ad_service">Business Service Ad</option>
|
||||||
|
<option value="ad_real_estate">Real Estate Ad</option>
|
||||||
|
<option value="nyakabyizi">Nyakabyizi (Village Only)</option>
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Post Content (Try adding a link or phone)">
|
||||||
|
<textarea
|
||||||
|
placeholder="Type something like 'Contact me at 078...'"
|
||||||
|
value={demoTitle}
|
||||||
|
onChange={(e) => setDemoTitle(e.target.value)}
|
||||||
|
className="w-full bg-white border-2 border-gray-200 rounded-2xl px-5 py-4 font-bold text-gray-700 focus:border-blue-500 focus:ring-0 transition-all outline-none h-32"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-10 lg:p-16 flex flex-col justify-center items-center text-center">
|
||||||
|
<div className="mb-8 p-6 bg-blue-50 rounded-[2.5rem] w-full max-w-sm border-2 border-blue-100 relative">
|
||||||
|
<p className="text-blue-600 font-black uppercase text-sm tracking-widest mb-2">Calculated Fee</p>
|
||||||
|
<p className="text-6xl font-black text-gray-900 tracking-tighter mb-2">
|
||||||
|
{demoFee.toLocaleString()} <span className="text-2xl">RWF</span>
|
||||||
|
</p>
|
||||||
|
{demoTitle.includes('07') || demoTitle.includes('http') ? (
|
||||||
|
<div className="bg-red-100 text-red-700 text-xs font-bold px-3 py-1 rounded-full inline-block animate-pulse">
|
||||||
|
LINK DETECTED: +1,500 RWF Fee
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full max-w-sm">
|
||||||
|
{isBalanceInsufficient ? (
|
||||||
|
<div className="bg-red-50 border-2 border-red-200 p-6 rounded-3xl mb-8">
|
||||||
|
<div className="flex items-center gap-2 text-red-600 mb-3 justify-center">
|
||||||
|
<BaseIcon path={mdiAlert} size={24} />
|
||||||
|
<span className="font-black text-lg italic">Locked!</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-red-800 font-bold leading-tight">
|
||||||
|
“Ntushobora gukomeza. Banza wongere amafaranga kuri konti yawe.”
|
||||||
|
</p>
|
||||||
|
<p className="text-red-500 text-sm mt-2 font-medium">
|
||||||
|
Your balance ({userBalance} RWF) is too low.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-green-50 border-2 border-green-200 p-6 rounded-3xl mb-8">
|
||||||
|
<p className="text-green-800 font-bold">
|
||||||
|
{currentUser ? 'Balance Sufficient!' : 'Login to Post'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
disabled={isBalanceInsufficient || !currentUser}
|
||||||
|
className={`w-full py-5 rounded-2xl font-black text-xl shadow-xl transition-all flex items-center justify-center gap-3 ${
|
||||||
|
isBalanceInsufficient || !currentUser
|
||||||
|
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||||
|
: 'bg-green-600 hover:bg-green-700 text-white hover:scale-105 active:scale-95'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isBalanceInsufficient ? 'LOCKED' : (currentUser ? 'POST NOW' : 'JOIN TO POST')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* For You Feed */}
|
||||||
|
<div className="mb-24">
|
||||||
|
<div className="flex items-center justify-between mb-12">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-4xl font-black text-gray-900 flex items-center gap-3 italic">
|
||||||
|
<BaseIcon path={mdiStar} size={36} className="text-yellow-500" />
|
||||||
|
For You
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-400 font-bold mt-1">Personalized Marketplace Ranking</p>
|
||||||
|
</div>
|
||||||
|
<Link href="/posts" className="bg-white hover:bg-gray-100 text-gray-900 border border-gray-200 px-6 py-3 rounded-2xl font-black flex items-center gap-2 transition-all shadow-sm">
|
||||||
|
View All <BaseIcon path={mdiChevronRight} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-10">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<div key={i} className="bg-white rounded-3xl p-4 shadow-lg">
|
||||||
|
<div className="bg-gray-100 animate-pulse h-56 rounded-2xl mb-4"></div>
|
||||||
|
<div className="bg-gray-100 animate-pulse h-6 rounded-lg w-3/4 mb-3"></div>
|
||||||
|
<div className="bg-gray-100 animate-pulse h-4 rounded-lg w-1/2"></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-10">
|
||||||
|
{posts.length > 0 ? posts.map((post: any) => (
|
||||||
|
<div key={post.id} className="bg-white rounded-[2rem] shadow-xl overflow-hidden hover:shadow-2xl transition-all border border-gray-50 group transform hover:-translate-y-2">
|
||||||
|
<div className="h-64 bg-gray-100 relative overflow-hidden">
|
||||||
|
{post.images && post.images.length > 0 ? (
|
||||||
|
<img src={post.images[0].url} alt={post.title} className="w-full h-full object-cover transition-transform group-hover:scale-110 duration-700" />
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full text-gray-300">
|
||||||
|
<BaseIcon path={mdiBriefcase} size={64} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="absolute top-6 left-6 flex flex-col gap-2">
|
||||||
|
<span className="bg-blue-600/90 backdrop-blur-md text-white px-4 py-1.5 rounded-full text-xs font-black uppercase tracking-widest shadow-lg">
|
||||||
|
{post.post_type?.replace('_', ' ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{post.is_boosted && (
|
||||||
|
<div className="absolute top-6 right-6">
|
||||||
|
<span className="bg-yellow-400 text-blue-900 px-4 py-1.5 rounded-full text-xs font-black shadow-lg border-2 border-white/20">
|
||||||
|
BOOSTED
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="p-8">
|
||||||
|
<h3 className="text-2xl font-black mb-3 text-gray-900 group-hover:text-blue-700 transition-colors line-clamp-1 italic">
|
||||||
|
{post.title}
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center text-gray-400 font-bold text-sm mb-6">
|
||||||
|
<BaseIcon path={mdiMapMarker} size={18} className="mr-2 text-green-500" />
|
||||||
|
{post.village?.name || 'Local Village'}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between pt-6 border-t border-gray-50">
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-gray-400 font-black uppercase tracking-widest mb-1">Fee To Reveal</p>
|
||||||
|
<span className="text-2xl font-black text-gray-900">
|
||||||
|
{post.required_fee_rwf ? `${post.required_fee_rwf}` : 'FREE'}
|
||||||
|
{post.required_fee_rwf ? <span className="text-xs ml-1">RWF</span> : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button className="bg-blue-50 hover:bg-blue-600 text-blue-700 hover:text-white px-5 py-3 rounded-2xl font-black transition-all shadow-sm">
|
||||||
|
View
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)) : (
|
||||||
|
<div className="col-span-full py-24 text-center bg-white rounded-[3rem] border-4 border-dashed border-gray-100">
|
||||||
|
<div className="p-8 bg-gray-50 rounded-full inline-block mb-8">
|
||||||
|
<BaseIcon path={mdiBriefcase} size={64} className="text-gray-200" />
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-400 font-black text-3xl italic tracking-tighter mb-4">No posts in your area yet.</p>
|
||||||
|
<p className="text-gray-400 font-bold max-w-sm mx-auto">Be the first to post a job or ad and start growing your business today!</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tiers Section */}
|
||||||
|
<div className="bg-gray-900 rounded-[3rem] p-12 md:p-20 shadow-3xl relative overflow-hidden">
|
||||||
|
<div className="absolute top-0 right-0 w-64 h-64 bg-blue-600/20 rounded-full blur-[100px]"></div>
|
||||||
|
<div className="absolute bottom-0 left-0 w-64 h-64 bg-green-600/20 rounded-full blur-[100px]"></div>
|
||||||
|
|
||||||
|
<div className="relative z-10">
|
||||||
|
<h2 className="text-4xl md:text-5xl font-black text-white text-center mb-6 italic tracking-tighter">Choose Your Power</h2>
|
||||||
|
<p className="text-gray-400 text-center mb-16 font-bold text-xl">Unlock higher visibility and exclusive discounts.</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-6">
|
||||||
|
{displayTiers.map((tier) => (
|
||||||
|
<div key={tier.name} className={`bg-white/5 backdrop-blur-lg border border-white/10 p-8 rounded-3xl text-center flex flex-col justify-between hover:bg-white/10 transition-all cursor-pointer group transform hover:-translate-y-2`}>
|
||||||
|
<div>
|
||||||
|
<p className="text-white font-black text-2xl mb-2 group-hover:text-yellow-400 transition-colors">{tier.name}</p>
|
||||||
|
<div className="h-1 w-8 bg-blue-500 mx-auto mb-6 rounded-full group-hover:w-16 transition-all"></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-black text-white tracking-tighter">{tier.price} <span className="text-xs">RWF</span></p>
|
||||||
|
<p className="text-[10px] text-gray-500 font-black uppercase mt-2 tracking-widest">Per Month</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SectionMain>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="bg-gray-950 text-white py-24 px-4 mt-32 border-t border-white/5">
|
||||||
|
<div className="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-4 gap-16">
|
||||||
|
<div className="col-span-1 md:col-span-2">
|
||||||
|
<h3 className="text-4xl font-black mb-8 tracking-tighter">MENYA<span className="text-yellow-400">HAFI</span></h3>
|
||||||
|
<p className="text-gray-400 font-bold text-lg leading-relaxed max-w-md">
|
||||||
|
Revolutionizing trust in Rwanda's digital marketplace.
|
||||||
|
Verified actions, transparent pricing, and instant connections.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-black uppercase tracking-widest text-blue-500 mb-8">Support</h4>
|
||||||
|
<ul className="space-y-6 text-gray-400 font-bold">
|
||||||
|
<li className="flex items-center gap-3">
|
||||||
|
<span className="text-white">Email:</span> ishlambosh@gmail.com
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-3">
|
||||||
|
<span className="text-white">WhatsApp:</span> 0793031666
|
||||||
|
</li>
|
||||||
|
<li><Link href="/help_center_articles" className="hover:text-blue-400 transition-colors">Help Center</Link></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-black uppercase tracking-widest text-blue-500 mb-8">Legal</h4>
|
||||||
|
<ul className="space-y-6 text-gray-400 font-bold">
|
||||||
|
<li><Link href="/privacy-policy" className="hover:text-white">Privacy Policy</Link></li>
|
||||||
|
<li><Link href="/terms-of-use" className="hover:text-white">Terms of Use</Link></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="max-w-6xl mx-auto border-t border-white/5 mt-20 pt-10 flex flex-col md:flex-row justify-between items-center gap-6">
|
||||||
|
<p className="text-gray-600 font-bold text-sm">
|
||||||
|
© 2026 MENYAHAFI. Empowering Rwanda's Workforce.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-8 text-gray-600 font-black text-xs uppercase tracking-widest">
|
||||||
|
<span>Made with ❤️ in Kigali</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const videoBlock = (video) => {
|
|
||||||
if (video?.video_files?.length > 0) {
|
|
||||||
return (
|
|
||||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
|
||||||
<video
|
|
||||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
|
||||||
autoPlay
|
|
||||||
loop
|
|
||||||
muted
|
|
||||||
>
|
|
||||||
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
|
||||||
Your browser does not support the video tag.
|
|
||||||
</video>
|
|
||||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
|
||||||
<a
|
|
||||||
className='text-[8px]'
|
|
||||||
href={video?.user?.url}
|
|
||||||
target='_blank'
|
|
||||||
rel='noreferrer'
|
|
||||||
>
|
|
||||||
Video by {video.user.name} on Pexels
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={
|
|
||||||
contentPosition === 'background'
|
|
||||||
? {
|
|
||||||
backgroundImage: `${
|
|
||||||
illustrationImage
|
|
||||||
? `url(${illustrationImage.src?.original})`
|
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
|
||||||
}`,
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'left center',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Head>
|
|
||||||
<title>{getPageTitle('Starter Page')}</title>
|
|
||||||
</Head>
|
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
|
||||||
<div
|
|
||||||
className={`flex ${
|
|
||||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
|
||||||
} min-h-screen w-full`}
|
|
||||||
>
|
|
||||||
{contentType === 'image' && contentPosition !== 'background'
|
|
||||||
? imageBlock(illustrationImage)
|
|
||||||
: null}
|
|
||||||
{contentType === 'video' && contentPosition !== 'background'
|
|
||||||
? videoBlock(illustrationVideo)
|
|
||||||
: null}
|
|
||||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
|
||||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
|
||||||
<CardBoxComponentTitle title="Welcome to your MENYAHAFI app!"/>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
|
||||||
<p className='text-center text-gray-500'>For guides and documentation please check
|
|
||||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<BaseButtons>
|
|
||||||
<BaseButton
|
|
||||||
href='/login'
|
|
||||||
label='Login'
|
|
||||||
color='info'
|
|
||||||
className='w-full'
|
|
||||||
/>
|
|
||||||
|
|
||||||
</BaseButtons>
|
|
||||||
</CardBox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SectionFullScreen>
|
|
||||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
|
||||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
|
||||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
|
||||||
Privacy Policy
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
MenyahafiLanding.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutGuest>{page}</LayoutGuest>;
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
mdiChartTimelineVariant,
|
mdiChartTimelineVariant,
|
||||||
mdiUpload,
|
mdiUpload,
|
||||||
|
mdiAccountDetails,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import React, { ReactElement, useEffect, useState } from 'react';
|
import React, { ReactElement, useEffect, useState } from 'react';
|
||||||
@ -29,6 +30,7 @@ import { update, fetch } from '../stores/users/usersSlice';
|
|||||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import {findMe} from "../stores/authSlice";
|
import {findMe} from "../stores/authSlice";
|
||||||
|
import UserAvatarWithBadge from '../components/UserAvatarWithBadge';
|
||||||
|
|
||||||
const EditUsers = () => {
|
const EditUsers = () => {
|
||||||
const { currentUser, isFetching, token } = useAppSelector(
|
const { currentUser, isFetching, token } = useAppSelector(
|
||||||
@ -45,7 +47,18 @@ const EditUsers = () => {
|
|||||||
app_role: '',
|
app_role: '',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
avatar: [],
|
avatar: [],
|
||||||
password: ''
|
password: '',
|
||||||
|
national_id: '',
|
||||||
|
address: '',
|
||||||
|
birth_day: '',
|
||||||
|
birth_year: '',
|
||||||
|
account_type: '',
|
||||||
|
account_tier: '',
|
||||||
|
province: '',
|
||||||
|
district: '',
|
||||||
|
sector: '',
|
||||||
|
cell: '',
|
||||||
|
village: '',
|
||||||
};
|
};
|
||||||
const [initialValues, setInitialValues] = useState(initVals);
|
const [initialValues, setInitialValues] = useState(initVals);
|
||||||
|
|
||||||
@ -54,7 +67,15 @@ const EditUsers = () => {
|
|||||||
const newInitialVal = { ...initVals };
|
const newInitialVal = { ...initVals };
|
||||||
|
|
||||||
Object.keys(initVals).forEach(
|
Object.keys(initVals).forEach(
|
||||||
(el) => (newInitialVal[el] = currentUser[el]),
|
(el) => {
|
||||||
|
if (currentUser[el]) {
|
||||||
|
if (typeof currentUser[el] === 'object' && currentUser[el].id) {
|
||||||
|
newInitialVal[el] = currentUser[el].id;
|
||||||
|
} else {
|
||||||
|
newInitialVal[el] = currentUser[el];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
setInitialValues(newInitialVal);
|
setInitialValues(newInitialVal);
|
||||||
@ -64,7 +85,6 @@ const EditUsers = () => {
|
|||||||
const handleSubmit = async (data) => {
|
const handleSubmit = async (data) => {
|
||||||
await dispatch(update({ id: currentUser.id, data }));
|
await dispatch(update({ id: currentUser.id, data }));
|
||||||
await dispatch(findMe());
|
await dispatch(findMe());
|
||||||
await router.push('/users/users-list');
|
|
||||||
notify('success', 'Profile was updated!');
|
notify('success', 'Profile was updated!');
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -75,46 +95,88 @@ const EditUsers = () => {
|
|||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton
|
<SectionTitleLineWithButton
|
||||||
icon={mdiChartTimelineVariant}
|
icon={mdiAccountDetails}
|
||||||
title='Edit profile'
|
title='My Profile'
|
||||||
main
|
main
|
||||||
>
|
>
|
||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox>
|
<CardBox>
|
||||||
{currentUser?.avatar[0]?.publicUrl && <div className={'grid grid-cols-6 gap-4 mb-4'}>
|
<div className="flex flex-col items-center mb-8">
|
||||||
<div className="col-span-1 w-80 h-80 overflow-hidden border-2 rounded-full inline-flex items-center justify-center mb-8">
|
<UserAvatarWithBadge user={currentUser} size={160} />
|
||||||
<img className="w-80 h-80 max-w-full max-h-full object-cover object-center" src={`${currentUser?.avatar[0]?.publicUrl}`} alt="Avatar" />
|
<h2 className="text-2xl font-bold mt-4">{currentUser?.firstName} {currentUser?.lastName}</h2>
|
||||||
</div>
|
<p className="text-gray-500 font-bold uppercase text-sm">{currentUser?.account_tier?.name || 'Starter'}</p>
|
||||||
</div>}
|
</div>
|
||||||
|
|
||||||
<Formik
|
<Formik
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={(values) => handleSubmit(values)}
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<FormField>
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<Field
|
<FormField label='Avatar'>
|
||||||
label='Avatar'
|
<Field
|
||||||
color='info'
|
color='info'
|
||||||
icon={mdiUpload}
|
icon={mdiUpload}
|
||||||
path={'users/avatar'}
|
path={'users/avatar'}
|
||||||
name='avatar'
|
name='avatar'
|
||||||
id='avatar'
|
id='avatar'
|
||||||
schema={{
|
component={FormImagePicker}
|
||||||
size: undefined,
|
></Field>
|
||||||
formats: undefined,
|
</FormField>
|
||||||
}}
|
|
||||||
component={FormImagePicker}
|
<div className="space-y-4">
|
||||||
></Field>
|
<FormField label='First Name'>
|
||||||
</FormField>
|
<Field name='firstName' placeholder='First Name' />
|
||||||
<FormField label='First Name'>
|
</FormField>
|
||||||
<Field name='firstName' placeholder='First Name' />
|
|
||||||
|
<FormField label='Last Name'>
|
||||||
|
<Field name='lastName' placeholder='Last Name' />
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BaseDivider />
|
||||||
|
<h3 className="text-lg font-bold mb-4">Identification & Location</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<FormField label='National ID (16 digits)'>
|
||||||
|
<Field name='national_id' placeholder='National ID' />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label='Account Type'>
|
||||||
|
<Field name='account_type' component={SelectField} options={[
|
||||||
|
{ id: 'Worker', name: 'Worker' },
|
||||||
|
{ id: 'Employer', name: 'Employer' },
|
||||||
|
{ id: 'Business', name: 'Business' },
|
||||||
|
{ id: 'Government', name: 'Government' },
|
||||||
|
{ id: 'NGO', name: 'NGO' },
|
||||||
|
]} />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label='Birth Day'>
|
||||||
|
<Field name='birth_day' type="number" placeholder='Day' />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label='Birth Year'>
|
||||||
|
<Field name='birth_year' type="number" placeholder='Year' />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label='Province'>
|
||||||
|
<Field name='province' component={SelectField} itemRef={'provinces'} showField={'name'} />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label='District'>
|
||||||
|
<Field name='district' component={SelectField} itemRef={'districts'} showField={'name'} />
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField label='Address'>
|
||||||
|
<Field name='address' placeholder='Detailed Address' />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label='Last Name'>
|
<BaseDivider />
|
||||||
<Field name='lastName' placeholder='Last Name' />
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField label='Phone Number'>
|
<FormField label='Phone Number'>
|
||||||
<Field name='phoneNumber' placeholder='Phone Number' />
|
<Field name='phoneNumber' placeholder='Phone Number' />
|
||||||
@ -124,46 +186,8 @@ const EditUsers = () => {
|
|||||||
<Field name='email' placeholder='E-Mail' disabled />
|
<Field name='email' placeholder='E-Mail' disabled />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label='App Role' labelFor='app_role'>
|
|
||||||
<Field
|
|
||||||
name='app_role'
|
|
||||||
id='app_role'
|
|
||||||
component={SelectField}
|
|
||||||
options={initialValues.app_role}
|
|
||||||
itemRef={'roles'}
|
|
||||||
showField={'name'}
|
|
||||||
></Field>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField label='Disabled' labelFor='disabled'>
|
|
||||||
<Field
|
|
||||||
name='disabled'
|
|
||||||
id='disabled'
|
|
||||||
component={SwitchField}
|
|
||||||
></Field>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="Password"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
name="password"
|
|
||||||
placeholder="password"
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<BaseDivider />
|
|
||||||
|
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton type='submit' color='info' label='Submit' />
|
<BaseButton type='submit' color='info' label='Update Profile' />
|
||||||
<BaseButton type='reset' color='info' outline label='Reset' />
|
|
||||||
<BaseButton
|
|
||||||
type='reset'
|
|
||||||
color='danger'
|
|
||||||
outline
|
|
||||||
label='Cancel'
|
|
||||||
onClick={() => router.push('/users/users-list')}
|
|
||||||
/>
|
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user