diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index b7d6cac..51f6355 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -20,6 +20,12 @@ import { currencyValues, cryptoRates, } from "@/components/dashboard/home"; +import { useRouter } from "next/navigation"; +import WithdrawOptionsMobileScreen from "@/components/dashboard/withdrawal/withdraw-options-mobile-screen"; +import WithdrawalModal from "@/components/dashboard/modals/withdrawal-modal"; +import { useWithdrawalStore } from "@/store/withdrawalStore"; +import WithdrawalSuccessModal from "@/components/dashboard/modals/withdraw-success-modal"; +import Image from "next/image"; const transactionData = [ { @@ -57,6 +63,13 @@ const transactionData = [ export default function DashboardContent() { const [balanceVisible, setBalanceVisible] = useState(true); const [selectedCurrency, setSelectedCurrency] = useState("ETH"); + const { + isMobileWithdrawOptionsOpen, + isWithdrawModalOpen, + isSuccessModalOpen, + openMobileWithdrawOptions, + } = useWithdrawalStore(); + const router = useRouter(); return (
@@ -119,7 +132,12 @@ export default function DashboardContent() {
- + router.push("/dashboard/withdrawal")} + />
@@ -131,10 +149,10 @@ export default function DashboardContent() { variant="ghost" className="flex p-[0.625rem] justify-start w-fit items-center gap-[0.5rem] rounded-lg bg-bg-dropdown hover:bg-bg-dropdown-hover" > - c.code === selectedCurrency) - ?.icon + ?.icon || "" } alt={selectedCurrency} width={16} @@ -166,7 +184,13 @@ export default function DashboardContent() {
{mobileActions.map((action, index) => ( - + + action.label === "Withdrawal" && openMobileWithdrawOptions() + } + {...action} + /> ))}
@@ -192,6 +216,9 @@ export default function DashboardContent() {
+ {isMobileWithdrawOptionsOpen && } + {isWithdrawModalOpen && } + {isSuccessModalOpen && } ); } diff --git a/app/dashboard/withdrawal/page.tsx b/app/dashboard/withdrawal/page.tsx new file mode 100644 index 0000000..4069efd --- /dev/null +++ b/app/dashboard/withdrawal/page.tsx @@ -0,0 +1,84 @@ +"use client"; + +import WithdrawalSuccessModal from "@/components/dashboard/modals/withdraw-success-modal"; +import WithdrawalModal from "@/components/dashboard/modals/withdrawal-modal"; +import { useIsMobile } from "@/hooks/useIsMobile"; +import { useWithdrawalStore } from "@/store/withdrawalStore"; +import { + ArrowLeft, + ArrowLeftRight, + ArrowUp, + FolderSymlink, + SquareArrowOutUpRight, +} from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; + +export default function WithdrawalPage() { + const { isWithdrawModalOpen, openWithdrawModal, isSuccessModalOpen } = + useWithdrawalStore(); + const router = useRouter(); + const isMobile = useIsMobile(); + + useEffect(() => { + if (isMobile) { + router.replace("/dashboard"); + openWithdrawModal(); + } + }, [isMobile, openWithdrawModal, router]); + + return ( +
+
+

router.back()} + > + + Withdrawal +

+ +
{" "} +
+

Select a Deposit Method

+
+
openWithdrawModal()} + > +

+ + Withdraw to Wallet +

+

+ Send your crypto to any external wallet address — fast, secure, + and easy +

+

Fee: 0%

+
+ +
+
+

+ + Sell for Fiat (via MoonPay) +

+

+ Sell your crypto and receive fiat into your bank or card account +

+

Fee: 0%

+
+ +
+
+
+ {isWithdrawModalOpen && } + {isSuccessModalOpen && } +
+ ); +} diff --git a/components/dashboard/home/action-button.tsx b/components/dashboard/home/action-button.tsx index e4d8843..ef40fb4 100644 --- a/components/dashboard/home/action-button.tsx +++ b/components/dashboard/home/action-button.tsx @@ -2,15 +2,17 @@ import React from "react"; import { Button } from "@/components/ui/button"; interface ActionButtonProps { - icon: React.ComponentType; + icon: React.ComponentType>; label: string; variant?: "primary" | "secondary"; + onClick?: () => void; } export function ActionButton({ icon: Icon, label, variant = "primary", + onClick, }: ActionButtonProps) { const isPrimary = variant === "primary"; const variantClasses = isPrimary @@ -20,6 +22,7 @@ export function ActionButton({ return ( + + + + + ); +} diff --git a/components/dashboard/modals/withdrawal-modal.tsx b/components/dashboard/modals/withdrawal-modal.tsx new file mode 100644 index 0000000..6a76559 --- /dev/null +++ b/components/dashboard/modals/withdrawal-modal.tsx @@ -0,0 +1,222 @@ +import { AlertTriangle, XIcon } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import Image from "next/image"; +import { useFormik } from "formik"; +import { useIsMobile } from "@/hooks/useIsMobile"; +import { cn } from "@/lib/utils"; +import { useWithdrawalStore } from "@/store/withdrawalStore"; +import { withdrawalValidationSchema } from "@/utils/withdrawalSchema"; + +interface WithdrawModalData { + walletAddressOrUsername: string; + asset: string; + amount: number; +} + +const assets = [ + { + name: "USDC", + icon: "/usdc.svg", + }, + { + name: "ETH", + icon: "/eth.svg", + }, + { + name: "BNB", + icon: "/bnb.svg", + }, +]; + +export default function WithdrawalModal() { + const { isWithdrawModalOpen, closeWithdrawModal, openSuccessModal } = + useWithdrawalStore(); + const { + values, + handleChange, + handleSubmit, + handleBlur, + isValid, + setFieldValue, + touched, + errors, + isSubmitting, + } = useFormik({ + initialValues: { + walletAddressOrUsername: "", + asset: "USDC", + amount: 0, + }, + validationSchema: withdrawalValidationSchema, + validateOnMount: true, + onSubmit: async (values) => { + console.log("Submitting...", values); + + // Simulate network request + await new Promise((resolve) => setTimeout(resolve, 1500)); + + console.log("Submission successful"); + closeWithdrawModal(); + openSuccessModal(); + }, + }); + const isMobile = useIsMobile(); + + return ( +
closeWithdrawModal()} + > +
e.stopPropagation()} + > +
+

+ Withdraw to another wallet +

+ closeWithdrawModal()} /> +
+ +
+
+ + + {touched.walletAddressOrUsername && ( +

+ {errors.walletAddressOrUsername} +

+ )} +
+ +
+ + + + + + + + + {assets.map((asset) => ( + setFieldValue("asset", asset.name)} + className="flex items-center space-x-3 p-2 cursor-pointer hover:bg-gray-100" + > + {asset.name} + {asset.name} + + ))} + + +
+ +
+ +
+ + +

+ Balance + 326,447 +

+

setFieldValue("amount", "326447")} + > + max +

+
+ {touched.amount && ( +

{errors.amount}

+ )} +
+ +
+ +

Reconfirm Wallet address

+
+ +
+ + +
+
+
+
+ ); +} diff --git a/components/dashboard/withdrawal/withdraw-options-mobile-screen.tsx b/components/dashboard/withdrawal/withdraw-options-mobile-screen.tsx new file mode 100644 index 0000000..81768cb --- /dev/null +++ b/components/dashboard/withdrawal/withdraw-options-mobile-screen.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { + Dialog, + DialogClose, + DialogContent, + DialogOverlay, + DialogPortal, + DialogTitle, +} from "@/components/ui/dialog"; +import { useWithdrawalStore } from "@/store/withdrawalStore"; +import { + ArrowLeftRight, + FolderSymlink, + SquareArrowOutUpRight, + XIcon, +} from "lucide-react"; + +export default function WithdrawOptionsMobileScreen() { + const { + isMobileWithdrawOptionsOpen, + closeMobileWithdrawOptions, + openWithdrawModal, + } = useWithdrawalStore(); + + const handleShowWithdrawModal = () => { + closeMobileWithdrawOptions(); + openWithdrawModal(); + }; + return ( + + + + + Withdraw +
+

Select a Deposit Method

+
+
+

+ + Withdraw to Wallet +

+

+ Send your crypto to any external wallet address — fast, + secure, and easy +

+

Fee: 0%

+
+
+
+

+ + Sell for Fiat (via MoonPay) +

+

+ Sell your crypto and receive fiat into your bank or card + account +

+

Fee: 0%

+
+ +
+
+
+ + + +
+
+
+ ); +} diff --git a/hooks/useIsMobile.ts b/hooks/useIsMobile.ts new file mode 100644 index 0000000..6858f1d --- /dev/null +++ b/hooks/useIsMobile.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from "react"; + +export function useIsMobile(breakpoint = 768) { + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`); + const onChange = (e: MediaQueryListEvent) => setIsMobile(e.matches); + setIsMobile(mql.matches); + mql.addEventListener("change", onChange); + return () => mql.removeEventListener("change", onChange); + }, [breakpoint]); + + return isMobile; +} diff --git a/public/green-tick.svg b/public/green-tick.svg new file mode 100644 index 0000000..807a307 --- /dev/null +++ b/public/green-tick.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/usdc.svg b/public/usdc.svg new file mode 100644 index 0000000..8f15be1 --- /dev/null +++ b/public/usdc.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/store/withdrawalStore.ts b/store/withdrawalStore.ts new file mode 100644 index 0000000..9f98c04 --- /dev/null +++ b/store/withdrawalStore.ts @@ -0,0 +1,28 @@ +import { create } from "zustand"; + +interface WithdrawalState { + isMobileWithdrawOptionsOpen: boolean; + isWithdrawModalOpen: boolean; + isSuccessModalOpen: boolean; + openWithdrawModal: () => void; + closeWithdrawModal: () => void; + openMobileWithdrawOptions: () => void; + closeMobileWithdrawOptions: () => void; + openSuccessModal: () => void; + closeSuccessModal: () => void; + toggleWithdrawModal: () => void; +} + +export const useWithdrawalStore = create((set) => ({ + isMobileWithdrawOptionsOpen: false, + isWithdrawModalOpen: false, + isSuccessModalOpen: false, + openWithdrawModal: () => set({ isWithdrawModalOpen: true }), + closeWithdrawModal: () => set({ isWithdrawModalOpen: false }), + openMobileWithdrawOptions: () => set({ isMobileWithdrawOptionsOpen: true }), + closeMobileWithdrawOptions: () => set({ isMobileWithdrawOptionsOpen: false }), + openSuccessModal: () => set({ isSuccessModalOpen: true }), + closeSuccessModal: () => set({ isSuccessModalOpen: false }), + toggleWithdrawModal: () => + set((state) => ({ isWithdrawModalOpen: !state.isWithdrawModalOpen })), +})); diff --git a/utils/withdrawalSchema.ts b/utils/withdrawalSchema.ts new file mode 100644 index 0000000..b08cdf7 --- /dev/null +++ b/utils/withdrawalSchema.ts @@ -0,0 +1,32 @@ +import * as Yup from "yup"; + +export const withdrawalValidationSchema = Yup.object({ + walletAddressOrUsername: Yup.string() + .required("Wallet address or username is required") + .min(3, "Must be at least 3 characters") + .test( + "valid-format", + "Invalid wallet address or username format", + (value) => { + if (!value) return false; + + // Check for Ethereum address pattern (0x followed by 40 hex characters) + const ethAddressPattern = /^0x[a-fA-F0-9]{40}$/; + // Check for username pattern (alphanumeric with underscores/hyphens) + const usernamePattern = /^[a-zA-Z0-9_-]{3,30}$/; + + return ethAddressPattern.test(value) || usernamePattern.test(value); + } + ), + + amount: Yup.number() + .required("Amount is required") + .positive("Amount must be positive") + .min(0.01, "Minimum amount is 0.01") + .max(326447, "Amount exceeds available balance") + .test("decimal-places", "Maximum 6 decimal places allowed", (value) => { + if (!value) return true; + const decimalPlaces = (value.toString().split(".")[1] || "").length; + return decimalPlaces <= 6; + }), +});