Commit 7499db99 by Arjun Jhukal

made the minimum and maxmimum deposit and withdraw dynamic and also added the…

made the minimum and maxmimum deposit and withdraw dynamic and also added the site visibility control at admin
parent 20162342
......@@ -9,7 +9,7 @@
!.yarn/plugins
!.yarn/releases
!.yarn/versions
.claude
# testing
/coverage
......
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
npm run dev # Start dev server on port 3000
npm run build # Production build
npm run lint # Run ESLint
```
There is a Jest test setup (`jest.config.js`, `jest.setup.tsx`) but tests are sparse. Run tests with:
```bash
npx jest # All tests
npx jest path/to/file.test.tsx # Single file
```
## Environment
```
NEXT_PUBLIC_FRONTEND_URL=http://localhost:3000
NEXT_PUBLIC_BASE_URL=https://app.getfirekirin.com # Backend API
NEXT_PUBLIC_FORT_PAY_KEY=... # CollectJS payment key
NEXT_PUBLIC_GTM_ID=...
NEXT_PUBLIC_AGE_CHECKER_KEY=...
```
All backend calls are proxied: Next.js rewrites `/api/backend/*``NEXT_PUBLIC_BASE_URL`. The base query in `src/services/baseQuery.ts` prepends `/api/backend` to every RTK Query request and injects `Authorization`, `X-Device-Id`, and `X-Device-Fingerprint` headers automatically.
## Architecture
### Routing (Next.js App Router)
Route groups in `src/app/` control layouts and auth — parentheses folders add no URL segment:
- `(auth)/` — public auth pages (`/login`, `/register`, etc.)
- `(dashboard)/(admin)/` — admin-only pages (`/settings`, `/games`, `/players`, `/transactions`, etc.), wrapped in `Private` + role check
- `(dashboard)/(user)/(outsideAuth)/` — public user pages (home, buy-coins, withdrawl, etc.), wrapped in `ComingSoonGate`
- `(dashboard)/(user)/(privateUser)/` — authenticated user pages (`/profile/...`), wrapped in `ComingSoonGate` + `Private`
Admin and user routes share the same URL namespace (no `/admin/` prefix). Role differentiation happens at the layout/component level via the `Private` component and `user.role` checks (`SUPER_ADMIN | ADMIN | USER`).
### State Management
Redux store (`src/hooks/store.ts`) combines 5 slices + 14 RTK Query API reducers:
**Slices (`src/slice/`):**
- `authSlice` — auth token + user object, persisted to localStorage as `"token"` JSON
- `toastSlice` — global toast notifications (`showToast({ message, variant })`)
- `authModalSlice` — login modal open/close state
- `updatePasswordSlice` — game password change modal + provider context
- `userBalanceSlice` — per-provider game balances cache
**RTK Query APIs (`src/services/`):**
| Service | Key endpoints |
|---|---|
| `authApi` | login, register, verify email, OTP, age verification |
| `gameApi` | game CRUD (admin), user game listing |
| `playerApi` | user/player CRUD (admin) |
| `userApi` | profile, game credentials, password changes |
| `transactionApi` | user balance, deposits (`deposit`), withdrawals (`submitMassPayPaymentFields`) |
| `settingApi` | site config, banners, chatbot, transaction limits, site availability |
| `pageApi` | CMS pages |
| `paymentSetupApi` | payment gateway config (admin) |
| `dashboardApi` | admin analytics |
Tags for cache invalidation use string literals (`'settings'`, `'Deposit'`, etc.) — check existing tags before adding new endpoints.
### Server-Side Fetching
`src/serverApi/` contains plain async functions (not RTK Query) for SSR/ISR. They call `serverBaseQuery()` directly. Used in root `layout.tsx` for SEO metadata and in page components that need server-rendered data.
### Auth Flow
1. Login stores `{ access_token, user }` in Redux + localStorage key `"token"`
2. `src/routes/Private.tsx` hydrates auth from localStorage/cookies on mount, validates JWT expiry
3. `src/utils/authSession.ts` handles backup/restore across payment redirects (Fort Pay redirects away and back)
4. Token is injected into every RTK Query request via `prepareHeaders` in `baseQuery.ts`
### Payment Flow (Fort Pay / CollectJS)
CollectJS (loaded via `<Script>`) tokenizes card data client-side. Key pattern in `FortPay.tsx`:
- Use a `useRef` (`billingRef`) to pass Formik values into the CollectJS callback closure — the callback is created at script-load time and cannot close over reactive state
- The submit button must be `type="button"` calling `formik.submitForm()` manually — `type="submit"` causes CollectJS to intercept the form and bypass Yup validation
- Billing fields must be sent to the backend API explicitly; `wallet.billingInfo` is only populated for Apple Pay/Google Pay, never for CC tokenization
### Coming Soon Gate
`src/components/organism/ComingSoonGate.tsx` fetches `/api/setting/site-availability` (public endpoint). When `coming_soon === false`, it renders the coming soon page instead of children. Applied only to user layouts — admin layouts are unaffected. Default value is `true` (site is live).
### Styling
- **TailwindCSS v4** for utility classes
- **Material-UI v7** for components — theme customized in `src/theme/`
- **ThemeContext** (`src/context/ThemeContext.tsx`) manages dark/light mode, locale (en/ar), and sidebar mini mode
### Key Conventions
- Path alias: `@/``src/` (configured in `tsconfig.json`)
- All route path constants live in `src/routes/PATH.ts` — use these instead of hardcoded strings
- Form handling: Formik + Yup throughout. Dynamic Yup schemas (e.g., transaction limits) are built with factory functions that accept nullable config values
- Nullable limits pattern: `null` means "no limit enforced"; UI elements conditionally render only when the value is non-null (never show `$null` or empty parentheses)
- Admin public API endpoints: `/api/admin/...` (requires auth). Public equivalents use `/api/setting/...` (e.g., chatbot, site availability)
- `enableReinitialize: true` is used on Formik forms that populate from RTK Query data
......@@ -2,6 +2,7 @@
import DashboardLayout from '@/components/layouts/DashboardLayout';
import AgeVerificationModal from '@/components/organism/dialog';
import ComingSoonGate from '@/components/organism/ComingSoonGate';
import { useSearchParams } from 'next/navigation';
import React, { Suspense, useEffect } from 'react';
......@@ -17,14 +18,15 @@ function LayoutContent({ children }: { children: React.ReactNode }) {
}
}, [visitorId]);
return (
<DashboardLayout>
{children}
<AgeVerificationModal />
<ComingSoonGate>
<DashboardLayout>
{children}
<AgeVerificationModal />
{/* <AgeGate /> */}
{/* <AgeGate /> */}
</DashboardLayout>
</DashboardLayout>
</ComingSoonGate>
)
}
export default function DashboardRootLayout({ children }: { children: React.ReactNode }) {
......
import DashboardLayout from '@/components/layouts/DashboardLayout'
import ComingSoonGate from '@/components/organism/ComingSoonGate'
import Private from '@/routes/Private'
import React from 'react'
export default function PrivateUserLayout({ children }: { children: React.ReactNode }) {
return (
<Private>
<DashboardLayout>
{children}
</DashboardLayout>
</Private>
<ComingSoonGate>
<Private>
<DashboardLayout>
{children}
</DashboardLayout>
</Private>
</ComingSoonGate>
)
}
"use client";
import ComingSoon from "@/components/pages/ComingSoon";
import { useGetSiteAvailabilityQuery } from "@/services/settingApi";
import React from "react";
export default function ComingSoonGate({ children }: { children: React.ReactNode }) {
const { data, isLoading } = useGetSiteAvailabilityQuery();
if (isLoading) return null;
if (data?.data?.coming_soon === false) {
return <ComingSoon />;
}
return <>{children}</>;
}
"use client";
import Image from "next/image";
export default function ComingSoon() {
return (
<div className="min-h-screen flex flex-col items-center justify-center px-6 text-center"
style={{
background: "linear-gradient(135deg, #0d1117 0%, #1a1f2e 50%, #0d1117 100%)",
}}
>
<div className="max-w-lg w-full flex flex-col items-center gap-6">
<Image
src="/assets/images/logo.png"
alt="Logo"
width={120}
height={120}
className="rounded-full mb-2"
/>
<div>
<h1 className="text-[40px] lg:text-[56px] font-bold text-white leading-tight">
Coming Soon
</h1>
<p className="text-[14px] lg:text-[16px] text-white/60 mt-3">
We&apos;re working hard to bring you something amazing. Check back soon!
</p>
</div>
</div>
</div>
);
}
"use client";
import React from "react";
import Link from "next/link";
import { useAppDispatch } from "@/hooks/hook";
import { PATH } from "@/routes/PATH";
import { useForgotPasswordMutation } from "@/services/authApi";
import { showToast, ToastVariant } from "@/slice/toastSlice";
import {
Box,
InputLabel,
OutlinedInput,
} from "@mui/material";
import AuthMessageBlock from "../authMessageBlock";
import { useFormik } from "formik";
import * as Yup from "yup";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useAppDispatch } from "@/hooks/hook";
import { useForgotPasswordMutation } from "@/services/authApi";
import { showToast, ToastVariant } from "@/slice/toastSlice";
import * as Yup from "yup";
import AuthMessageBlock from "../authMessageBlock";
export default function ForgotPasswordPage() {
const router = useRouter();
......@@ -66,7 +65,7 @@ export default function ForgotPasswordPage() {
dispatch(
showToast({
message: error.message || "Something went wrong",
message: error?.data?.message || "Something went wrong",
variant: ToastVariant.ERROR,
})
);
......
"use client";
import { useAppDispatch } from "@/hooks/hook";
import { useGetSiteAvailabilityQuery, useUpdateSiteAvailabilityMutation } from "@/services/settingApi";
import { showToast, ToastVariant } from "@/slice/toastSlice";
import { Button, FormControlLabel, Switch } from "@mui/material";
import { useFormik } from "formik";
export default function SiteAvailability() {
const dispatch = useAppDispatch();
const { data } = useGetSiteAvailabilityQuery();
const [updateAvailability] = useUpdateSiteAvailabilityMutation();
const formik = useFormik({
initialValues: {
coming_soon: data?.data?.coming_soon ?? false,
},
enableReinitialize: true,
onSubmit: async (values) => {
try {
const response = await updateAvailability({ coming_soon: values.coming_soon }).unwrap();
dispatch(showToast({ message: response.message || "Saved successfully", variant: ToastVariant.SUCCESS }));
} catch (e: any) {
dispatch(showToast({ message: e?.data?.message || "Something went wrong", variant: ToastVariant.ERROR }));
}
},
});
return (
<form onSubmit={formik.handleSubmit}>
<div className="form__field__wrapper border border-gray rounded-[16px] mb-6">
<div className="form__title py-6 px-10 border-b border-gray">
<h2 className="text-[20px] font-bold">Site Availability</h2>
<p className="text-[13px] mt-1 opacity-60">Enable to show a "Coming Soon" page to all users</p>
</div>
<div className="form__fields p-6 lg:p-10">
<FormControlLabel
control={
<Switch
checked={formik.values.coming_soon}
onChange={(e) => formik.setFieldValue("coming_soon", e.target.checked)}
color="primary"
/>
}
label="Coming Soon Mode"
/>
</div>
</div>
<div className="text-right">
<Button type="submit" variant="contained" color="primary">
Save Settings
</Button>
</div>
</form>
);
}
"use client";
import { useAppDispatch } from "@/hooks/hook";
import { useGetTransactionLimitsQuery, useUpdateTransactionLimitsMutation } from "@/services/settingApi";
import { showToast, ToastVariant } from "@/slice/toastSlice";
import { Button, InputLabel, OutlinedInput } from "@mui/material";
import { useFormik } from "formik";
import * as Yup from "yup";
const validationSchema = Yup.object({
min_deposit: Yup.number().nullable().min(0, "Must be 0 or greater"),
max_deposit: Yup.number().nullable().min(0, "Must be 0 or greater")
.test("max-gt-min", "Max deposit must be greater than min deposit", function (max) {
const min = this.parent.min_deposit;
if (max == null || min == null) return true;
return max > min;
}),
min_withdrawal: Yup.number().nullable().min(0, "Must be 0 or greater"),
max_withdrawal: Yup.number().nullable().min(0, "Must be 0 or greater")
.test("max-gt-min", "Max withdrawal must be greater than min withdrawal", function (max) {
const min = this.parent.min_withdrawal;
if (max == null || min == null) return true;
return max > min;
}),
});
export default function TransactionLimits() {
const dispatch = useAppDispatch();
const { data } = useGetTransactionLimitsQuery();
const [updateLimits] = useUpdateTransactionLimitsMutation();
const formik = useFormik({
initialValues: {
min_deposit: data?.data?.min_deposit ?? null,
max_deposit: data?.data?.max_deposit ?? null,
min_withdrawal: data?.data?.min_withdrawal ?? null,
max_withdrawal: data?.data?.max_withdrawal ?? null,
},
enableReinitialize: true,
validationSchema,
onSubmit: async (values) => {
try {
const response = await updateLimits({
min_deposit: values.min_deposit === "" ? null : values.min_deposit,
max_deposit: values.max_deposit === "" ? null : values.max_deposit,
min_withdrawal: values.min_withdrawal === "" ? null : values.min_withdrawal,
max_withdrawal: values.max_withdrawal === "" ? null : values.max_withdrawal,
}).unwrap();
dispatch(showToast({ message: response.message || "Saved successfully", variant: ToastVariant.SUCCESS }));
} catch (e: any) {
dispatch(showToast({ message: e?.data?.message || "Something went wrong", variant: ToastVariant.ERROR }));
}
},
});
const handleNumberChange = (field: string, value: string) => {
formik.setFieldValue(field, value === "" ? null : Number(value));
};
return (
<form onSubmit={formik.handleSubmit}>
{/* Deposit Limits */}
<div className="form__field__wrapper border border-gray rounded-[16px] mb-6">
<div className="form__title py-6 px-10 border-b border-gray">
<h2 className="text-[20px] font-bold">Deposit Limits</h2>
<p className="text-[13px] mt-1 opacity-60">Leave blank to apply no limit</p>
</div>
<div className="form__fields p-6 lg:p-10 grid gap-4 lg:gap-6 md:grid-cols-2">
<div className="input__field">
<InputLabel>Minimum Deposit ($)</InputLabel>
<OutlinedInput
fullWidth
type="number"
name="min_deposit"
placeholder="No minimum"
value={formik.values.min_deposit ?? ""}
onChange={(e) => handleNumberChange("min_deposit", e.target.value)}
onBlur={formik.handleBlur}
inputProps={{ min: 0, step: 1 }}
/>
<span className="error">
{formik.touched.min_deposit && formik.errors.min_deposit ? formik.errors.min_deposit : ""}
</span>
</div>
<div className="input__field">
<InputLabel>Maximum Deposit ($)</InputLabel>
<OutlinedInput
fullWidth
type="number"
name="max_deposit"
placeholder="No maximum"
value={formik.values.max_deposit ?? ""}
onChange={(e) => handleNumberChange("max_deposit", e.target.value)}
onBlur={formik.handleBlur}
inputProps={{ min: 0, step: 1 }}
/>
<span className="error">
{formik.touched.max_deposit && formik.errors.max_deposit ? formik.errors.max_deposit : ""}
</span>
</div>
</div>
</div>
{/* Withdrawal Limits */}
<div className="form__field__wrapper border border-gray rounded-[16px] mb-6">
<div className="form__title py-6 px-10 border-b border-gray">
<h2 className="text-[20px] font-bold">Withdrawal Limits</h2>
<p className="text-[13px] mt-1 opacity-60">Leave blank to apply no limit</p>
</div>
<div className="form__fields p-6 lg:p-10 grid gap-4 lg:gap-6 md:grid-cols-2">
<div className="input__field">
<InputLabel>Minimum Withdrawal ($)</InputLabel>
<OutlinedInput
fullWidth
type="number"
name="min_withdrawal"
placeholder="No minimum"
value={formik.values.min_withdrawal ?? ""}
onChange={(e) => handleNumberChange("min_withdrawal", e.target.value)}
onBlur={formik.handleBlur}
inputProps={{ min: 0, step: 1 }}
/>
<span className="error">
{formik.touched.min_withdrawal && formik.errors.min_withdrawal ? formik.errors.min_withdrawal : ""}
</span>
</div>
<div className="input__field">
<InputLabel>Maximum Withdrawal ($)</InputLabel>
<OutlinedInput
fullWidth
type="number"
name="max_withdrawal"
placeholder="No maximum"
value={formik.values.max_withdrawal ?? ""}
onChange={(e) => handleNumberChange("max_withdrawal", e.target.value)}
onBlur={formik.handleBlur}
inputProps={{ min: 0, step: 1 }}
/>
<span className="error">
{formik.touched.max_withdrawal && formik.errors.max_withdrawal ? formik.errors.max_withdrawal : ""}
</span>
</div>
</div>
</div>
<div className="text-right">
<Button type="submit" variant="contained" color="primary">
Save Settings
</Button>
</div>
</form>
);
}
......@@ -5,7 +5,9 @@ import { useState } from 'react';
import AdminProfile from './AdminProfile';
import BannerSlider from './BannerSlider';
import Chatbot from './Chatbot';
import SiteAvailability from './SiteAvailability';
import SiteSetting from './SiteSetting';
import TransactionLimits from './TransactionLimits';
export default function SettingPage() {
// Track the active tab index
......@@ -16,6 +18,8 @@ export default function SettingPage() {
{ title: "My Profile", content: <AdminProfile /> },
{ title: "Banner Slider", content: <BannerSlider /> },
{ title: "Chatbot", content: <Chatbot /> },
{ title: "Transaction Limits", content: <TransactionLimits /> },
{ title: "Site Availability", content: <SiteAvailability /> },
];
return (
......
......@@ -3,6 +3,7 @@
import GlassWrapper from '@/components/molecules/GlassWrapper';
import { useAppDispatch } from '@/hooks/hook';
import GoldCoinIcon from '@/icons/GoldCoinIcon';
import { useGetTransactionLimitsQuery } from '@/services/settingApi';
import { showToast, ToastVariant } from '@/slice/toastSlice';
import { Box, Button, OutlinedInput } from '@mui/material';
import { Coin } from '@wandersonalwes/iconsax-react';
......@@ -16,6 +17,9 @@ export default function CoinCalculator({ slug }: { slug: string }) {
const router = useRouter();
const dispatch = useAppDispatch();
const { data: limitsData } = useGetTransactionLimitsQuery();
const minDeposit = limitsData?.data?.min_deposit ?? 20;
const maxDeposit = limitsData?.data?.max_deposit ?? null;
// const calculateBonus = (amount: number) => {
// return Math.max(Math.round(25.56 * amount - 27.78), 0);
......@@ -40,13 +44,11 @@ export default function CoinCalculator({ slug }: { slug: string }) {
};
const handleBuy = () => {
if (Number(amount) < 20) {
return dispatch(
showToast({
message: "Minimum amount is 20",
variant: ToastVariant.ERROR,
})
)
if (Number(amount) < minDeposit) {
return dispatch(showToast({ message: `Minimum deposit is $${minDeposit}`, variant: ToastVariant.ERROR }));
}
if (maxDeposit !== null && Number(amount) > maxDeposit) {
return dispatch(showToast({ message: `Maximum deposit is $${maxDeposit}`, variant: ToastVariant.ERROR }));
}
router.push(`/buy-coins/${slug}/checkout?amount=${amount}&bonus=${amount}`);
};
......@@ -62,7 +64,7 @@ export default function CoinCalculator({ slug }: { slug: string }) {
}}>
<div className="title">
<h2 className='text-[28px]'>Custom</h2>
<span className='text-[12px]'>$1 = 100 Gold Coins</span>
<span className='text-[12px]'>$1 = 100 Gold Coins &nbsp;·&nbsp; Min ${minDeposit}{maxDeposit !== null ? ` / Max $${maxDeposit}` : ""}</span>
</div>
<div className="footer">
......
import GlassWrapper from '@/components/molecules/GlassWrapper'
import GoldCoinIcon from '@/icons/GoldCoinIcon'
import { Box } from '@mui/material'
import { Coin } from '@wandersonalwes/iconsax-react'
import Link from 'next/link'
import CoinCalculator from './CoinCalculator'
"use client";
import GlassWrapper from '@/components/molecules/GlassWrapper';
import GoldCoinIcon from '@/icons/GoldCoinIcon';
import { useGetTransactionLimitsQuery } from '@/services/settingApi';
import { Box } from '@mui/material';
import { Coin } from '@wandersonalwes/iconsax-react';
import Link from 'next/link';
import CoinCalculator from './CoinCalculator';
export default function BuyCoinSinlgeGame({ slug }: { slug: string }) {
const packs = [
......@@ -28,10 +30,14 @@ export default function BuyCoinSinlgeGame({ slug }: { slug: string }) {
},
]
const { data: limitsData } = useGetTransactionLimitsQuery();
const minDeposit = limitsData?.data?.min_deposit ?? null;
const maxDeposit = limitsData?.data?.max_deposit ?? null;
return (
<section className="buy__coin__root">
<div className="section__title mb-4 lg:mb-8 max-w-[520px]">
<h1 className='mb-2 text-[24px] lg:text-[32px]'>Buy Coins <span className="text-[#FBA027] text-[20px]">(Min Deposit $20)</span></h1>
<h1 className='mb-2 text-[24px] lg:text-[32px]'>Buy Coins <span className="text-[#FBA027] text-[20px]">{(minDeposit !== null || maxDeposit !== null) && (<span className="text-[#FBA027] text-[20px]"> ({minDeposit !== null ? `Min $${minDeposit}` : ""}{minDeposit !== null && maxDeposit !== null ? " – " : ""}{maxDeposit !== null ? `Max $${maxDeposit}` : ""})</span>)}</span></h1>
<p className='text-[11px] lg:text-[13px]'>To start playing and cashing out your winnings, you’ll need a crypto wallet to purchase E-Credits and receive payouts. Don't worry—it’s quick and easy!</p>
</div>
<div className="grid grid-col-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-4">
......
"use client";
import { useAppDispatch } from "@/hooks/hook";
import { useGetTransactionLimitsQuery } from "@/services/settingApi";
import { useSubmitMassPayPaymentFieldsMutation } from "@/services/transaction";
import { showToast, ToastVariant } from "@/slice/toastSlice";
import { openPasswordDialog } from "@/slice/updatePasswordSlice";
......@@ -14,18 +15,27 @@ import React from "react";
import * as Yup from "yup";
import WithdrawlModal from "./WithdrawlModal";
const validationSchema = Yup.object({
withdrawl_amounts: Yup.object().test(
"min-amount",
"Amount must be greater than $40",
(value) => {
if (!value) return true;
return Object.values(value).every(
(v) => v === "" || Number(v) >= 40
);
}
),
});
const buildValidationSchema = (min: number | null, max: number | null) =>
Yup.object({
withdrawl_amounts: Yup.object().test(
"amount-range",
"", // message set dynamically below
function (value) {
if (!value) return true;
for (const v of Object.values(value)) {
if (v === "" || v === undefined) continue;
const num = Number(v);
if (min !== null && num < min) {
return this.createError({ message: `Amount must be at least $${min}` });
}
if (max !== null && num > max) {
return this.createError({ message: `Amount must not exceed $${max}` });
}
}
return true;
}
),
});
export type WithdrawlFormValues = {
game_provider: string;
......@@ -68,6 +78,9 @@ export default function WithdrawlPage({
const dispatch = useAppDispatch();
const [withdrawMoney, { isLoading }] = useSubmitMassPayPaymentFieldsMutation();
const { data: limitsData } = useGetTransactionLimitsQuery();
const minWithdrawal = limitsData?.data?.min_withdrawal ?? null;
const maxWithdrawal = limitsData?.data?.max_withdrawal ?? null;
const formik = useFormik<WithdrawlFormValues>({
initialValues: {
......@@ -76,7 +89,7 @@ export default function WithdrawlPage({
type: "",
payment_fields: []
},
validationSchema,
validationSchema: buildValidationSchema(minWithdrawal, maxWithdrawal),
enableReinitialize: true,
onSubmit: async (values) => {
......@@ -154,13 +167,12 @@ export default function WithdrawlPage({
};
const handleWithdrawClick = (balance: number, provider: string) => {
if (balance < 40 || balance > 400) {
dispatch(
showToast({
message: "Withdraw Amount must be at least $40 and below $400",
variant: ToastVariant.ERROR,
})
);
if (minWithdrawal !== null && balance < minWithdrawal) {
dispatch(showToast({ message: `Withdraw amount must be at least $${minWithdrawal}`, variant: ToastVariant.ERROR }));
return;
}
if (maxWithdrawal !== null && balance > maxWithdrawal) {
dispatch(showToast({ message: `Withdraw amount must not exceed $${maxWithdrawal}`, variant: ToastVariant.ERROR }));
return;
}
formik.setFieldValue("game_provider", provider);
......@@ -181,7 +193,7 @@ export default function WithdrawlPage({
return (
<section className="withdrawl__root">
<div className="section__title mb-4 lg:mb-8 max-w-[560px]">
<h1 className="mb-2 text-[24px] lg:text-[32px]">Withdraw Coins <span className="text-[#FBA027] text-[20px]">(Min Withdrawl $40)</span></h1>
<h1 className="mb-2 text-[24px] lg:text-[32px]">Withdraw Coins{(minWithdrawal !== null || maxWithdrawal !== null) && (<span className="text-[#FBA027] text-[20px]"> ({minWithdrawal !== null ? `Min $${minWithdrawal}` : ""}{minWithdrawal !== null && maxWithdrawal !== null ? " – " : ""}{maxWithdrawal !== null ? `Max $${maxWithdrawal}` : ""})</span>)}</h1>
<p className="text-[11px] lg:text-[13px]">
To start playing and cashing out your winnings, you'll need a crypto
wallet to purchase E-Credits and receive payouts. Don't worry—it's quick
......@@ -288,7 +300,7 @@ export default function WithdrawlPage({
}
</span>
)}
<span className="text-[8px] lg:text-[10px]">Min $40.0</span>
{(minWithdrawal !== null || maxWithdrawal !== null) && (<span className="text-[8px] lg:text-[10px]">{minWithdrawal !== null ? `Min $${minWithdrawal}` : ""}{minWithdrawal !== null && maxWithdrawal !== null ? " / " : ""}{maxWithdrawal !== null ? `Max $${maxWithdrawal}` : ""}</span>)}
</div>
{/* Withdraw Button */}
......
import { GlobalResponse } from "@/types/config";
import { BannerResponseProps, ChatbotProps, SiteSettingResponseProps } from "@/types/setting";
import { BannerResponseProps, ChatbotProps, SiteAvailabilityResponse, SiteAvailabilitySettings, SiteSettingResponseProps, TransactionLimitResponse, TransactionLimitSettings } from "@/types/setting";
import { createApi } from "@reduxjs/toolkit/query/react";
import { baseQuery } from "./baseQuery";
......@@ -55,6 +55,39 @@ export const settingApi = createApi({
providesTags: ['Chatbot']
}),
getTransactionLimits: builder.query<TransactionLimitResponse, void>({
query: () => ({
url: "/api/settings/transaction-limits",
method: "GET",
}),
providesTags: ['settings']
}),
updateTransactionLimits: builder.mutation<GlobalResponse, TransactionLimitSettings>({
query: (body) => ({
url: "/api/admin/settings/transaction-limits",
method: "POST",
body,
}),
invalidatesTags: ['settings']
}),
getSiteAvailability: builder.query<SiteAvailabilityResponse, void>({
query: () => ({
url: "/api/settings/site-availability",
method: "GET",
}),
providesTags: ['settings']
}),
updateSiteAvailability: builder.mutation<GlobalResponse, SiteAvailabilitySettings>({
query: (body) => ({
url: "/api/admin/settings/site-availability",
method: "POST",
body,
}),
invalidatesTags: ['settings']
}),
})
})
......@@ -64,5 +97,10 @@ export const {
useUpdateBannerMutation,
useGetAllBannerQuery,
useUpdateChatbotMutation,
useGetChatbotSettingQuery
useGetChatbotSettingQuery,
useGetTransactionLimitsQuery,
useUpdateTransactionLimitsMutation,
useGetSiteAvailabilityQuery,
useUpdateSiteAvailabilityMutation,
} = settingApi;
\ No newline at end of file
......@@ -73,4 +73,27 @@ export interface ChatbotProps {
chatbot_image: File | null;
chatbot_image_url?: string;
chatbot_label: string;
}
export interface TransactionLimitSettings {
min_deposit: number | null;
max_deposit: number | null;
min_withdrawal: number | null;
max_withdrawal: number | null;
}
export interface TransactionLimitResponse {
success: boolean;
message: string;
data: TransactionLimitSettings;
}
export interface SiteAvailabilitySettings {
coming_soon: boolean;
}
export interface SiteAvailabilityResponse {
success: boolean;
message: string;
data: SiteAvailabilitySettings;
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment