Files
falah-mobile/src/app/wallet/page.tsx
T

411 lines
14 KiB
TypeScript
Raw Normal View History

"use client";
import { useState, useEffect, FormEvent } from "react";
import { useAuth } from "@/lib/AuthContext";
import { useRouter } from "next/navigation";
import {
Wallet,
Zap,
Crown,
TrendingUp,
ArrowUpRight,
ArrowDownLeft,
Clock,
Check,
X,
Loader2,
Sparkles,
Info,
} from "lucide-react";
interface CashoutEntry {
id: string;
amountFlh: number;
fiatAmount: number;
status: string;
createdAt: string;
}
const TOP_UP_OPTIONS = [
{ flh: 500, usd: 4.99, bonus: "" },
{ flh: 1100, usd: 9.99, bonus: "+10% bonus" },
{ flh: 3000, usd: 24.99, bonus: "+20% bonus" },
];
const EARNING_TIPS = [
"Post in the forum — earn 50 FLH per approved post",
"List items on Souq marketplace",
"Complete daily AI coaching sessions",
"Refer friends — earn 200 FLH per referral",
"Participate in community challenges",
];
export default function WalletPage() {
const { user, token, loading } = useAuth();
const router = useRouter();
const [balance, setBalance] = useState(0);
const [cashoutHistory, setCashoutHistory] = useState<CashoutEntry[]>([]);
const [cashoutAmount, setCashoutAmount] = useState("");
const [cashoutLoading, setCashoutLoading] = useState(false);
const [cashoutMessage, setCashoutMessage] = useState<{
type: "success" | "error";
text: string;
} | null>(null);
const [topupLoading, setTopupLoading] = useState<number | null>(null);
useEffect(() => {
if (!loading && !token) router.push("/login");
}, [loading, token, router]);
useEffect(() => {
if (token) {
fetchWallet();
}
}, [token]);
const fetchWallet = async () => {
try {
const res = await fetch("/api/wallet", {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) {
const data = await res.json();
setBalance(data.balance);
setCashoutHistory(data.cashoutHistory || []);
}
} catch {
// ignore
}
};
const handleTopUp = async (amount: number) => {
if (!token) return;
setTopupLoading(amount);
try {
const res = await fetch("/api/wallet/top-up/create-checkout", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ amount }),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || "Top-up failed");
}
const data = await res.json();
if (data.url) {
router.push(data.url);
}
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : "Top-up failed";
setCashoutMessage({ type: "error", text: message });
setTimeout(() => setCashoutMessage(null), 3000);
} finally {
setTopupLoading(null);
}
};
const handleCashout = async (e: FormEvent) => {
e.preventDefault();
if (!token) return;
const amountNum = parseInt(cashoutAmount, 10);
if (isNaN(amountNum) || amountNum < 1000) {
setCashoutMessage({
type: "error",
text: "Minimum cashout is 1,000 FLH",
});
setTimeout(() => setCashoutMessage(null), 3000);
return;
}
if (amountNum > balance) {
setCashoutMessage({
type: "error",
text: "Insufficient balance",
});
setTimeout(() => setCashoutMessage(null), 3000);
return;
}
setCashoutLoading(true);
setCashoutMessage(null);
try {
const res = await fetch("/api/wallet", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ amountFlh: amountNum }),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || "Cashout failed");
}
const data = await res.json();
setCashoutMessage({
type: "success",
text: `Cashout request for ${data.cashout.amountFlh} FLH submitted!`,
});
setCashoutAmount("");
await fetchWallet();
setTimeout(() => setCashoutMessage(null), 4000);
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : "Cashout failed";
setCashoutMessage({ type: "error", text: message });
setTimeout(() => setCashoutMessage(null), 3000);
} finally {
setCashoutLoading(false);
}
};
const isPremium = user?.isPremium || user?.isPro;
if (loading) {
return (
<div className="min-h-dvh bg-[#0a0a0f] flex items-center justify-center">
<div className="w-8 h-8 rounded-full border-2 border-[#D4AF37]/30 border-t-[#D4AF37] animate-spin" />
</div>
);
}
if (!user) return null;
return (
<div className="min-h-dvh bg-[#0a0a0f] pb-24">
<div className="px-4 pt-6 pb-2">
<h1 className="text-xl font-bold text-white">Wallet</h1>
</div>
<div className="px-4 space-y-5 animate-fade-in">
{/* Balance Card */}
<div className="bg-gradient-to-br from-[#D4AF37]/20 to-[#0a0a0f] border border-[#D4AF37]/30 rounded-2xl p-6 text-center">
<div className="w-14 h-14 rounded-full bg-[#D4AF37]/20 flex items-center justify-center mx-auto mb-3">
<Wallet size={24} className="text-[#D4AF37]" />
</div>
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">
FLH Balance
</p>
<p className="text-4xl font-bold text-[#D4AF37]">
{balance.toLocaleString()}
</p>
<p className="text-sm text-gray-500 mt-1">
${(balance / 1000).toFixed(2)} USD
</p>
{!isPremium && (
<div className="mt-4 bg-[#D4AF37]/10 border border-[#D4AF37]/20 rounded-xl px-4 py-3">
<div className="flex items-center justify-center gap-2">
<Crown size={14} className="text-[#D4AF37]" />
<p className="text-xs text-[#D4AF37] font-medium">
Premium members earn 2x FLH!
</p>
</div>
<button
onClick={() => router.push("/upgrade")}
className="mt-2 text-[11px] text-[#D4AF37] underline underline-offset-2"
>
Upgrade now
</button>
</div>
)}
</div>
{/* Messages */}
{cashoutMessage && (
<div
className={`flex items-center gap-2 p-3 rounded-xl text-sm ${
cashoutMessage.type === "success"
? "bg-emerald-900/20 border border-emerald-800/40 text-emerald-400"
: "bg-red-900/20 border border-red-800/40 text-red-400"
}`}
>
{cashoutMessage.type === "success" ? (
<Check size={16} />
) : (
<X size={16} />
)}
{cashoutMessage.text}
</div>
)}
{/* Top-Up Section */}
<div>
<h2 className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
<Zap size={16} className="text-[#D4AF37]" />
Top Up FLH
</h2>
<div className="grid grid-cols-3 gap-3">
{TOP_UP_OPTIONS.map((opt) => (
<button
key={opt.flh}
onClick={() => handleTopUp(opt.flh)}
disabled={topupLoading === opt.flh}
className="bg-[#111118] border border-gray-800/60 rounded-2xl p-4 text-center active:scale-95 transition hover:border-[#D4AF37]/30 disabled:opacity-60"
>
{topupLoading === opt.flh ? (
<Loader2 size={20} className="animate-spin mx-auto text-[#D4AF37]" />
) : (
<>
<p className="text-lg font-bold text-white">
{opt.flh.toLocaleString()}
</p>
<p className="text-xs text-gray-500 mt-0.5">
${opt.usd.toFixed(2)}
</p>
{opt.bonus && (
<p className="text-[10px] text-emerald-400 mt-1 font-medium">
{opt.bonus}
</p>
)}
</>
)}
</button>
))}
</div>
</div>
{/* Cashout Form */}
<div className="bg-[#111118] border border-gray-800/60 rounded-2xl p-5">
<h3 className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
<ArrowUpRight size={16} className="text-[#D4AF37]" />
Cash Out FLH
</h3>
<p className="text-xs text-gray-500 mb-3">
Convert your FLH to USD. Minimum 1,000 FLH (1,000 FLH = $1.00).
</p>
<form onSubmit={handleCashout} className="flex gap-3">
<div className="flex-1">
<input
type="number"
min={1000}
step={100}
value={cashoutAmount}
onChange={(e) => setCashoutAmount(e.target.value)}
placeholder="Amount FLH"
className="w-full bg-[#1a1a24] border border-gray-800/60 rounded-xl px-4 py-3 text-white placeholder-gray-600 focus:outline-none focus:border-[#D4AF37]/50 focus:ring-1 focus:ring-[#D4AF37]/30 transition"
/>
</div>
<button
type="submit"
disabled={cashoutLoading}
className="px-5 py-3 rounded-xl bg-[#D4AF37] text-[#0a0a0f] font-semibold text-sm disabled:opacity-50 active:opacity-80 transition flex items-center gap-2"
>
{cashoutLoading ? (
<Loader2 size={16} className="animate-spin" />
) : (
"Cash Out"
)}
</button>
</form>
{cashoutAmount && parseInt(cashoutAmount) >= 1000 && (
<p className="text-xs text-gray-500 mt-2">
You will receive ~$
{(parseInt(cashoutAmount) / 1000).toFixed(2)} USD
</p>
)}
</div>
{/* Transaction History */}
<div>
<h2 className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
<Clock size={16} className="text-gray-500" />
Cashout History
</h2>
{cashoutHistory.length === 0 ? (
<div className="bg-[#111118] border border-gray-800/60 rounded-2xl p-5 text-center">
<TrendingUp size={20} className="mx-auto text-gray-700 mb-2" />
<p className="text-sm text-gray-500">No cashouts yet</p>
<p className="text-xs text-gray-700 mt-1">
Your cashout requests will appear here
</p>
</div>
) : (
<div className="space-y-2">
{cashoutHistory.map((entry) => (
<div
key={entry.id}
className="bg-[#111118] border border-gray-800/60 rounded-xl px-4 py-3 flex items-center justify-between"
>
<div className="flex items-center gap-3">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center ${
entry.status === "approved"
? "bg-emerald-900/20"
: entry.status === "rejected"
? "bg-red-900/20"
: "bg-amber-900/20"
}`}
>
{entry.status === "approved" ? (
<ArrowDownLeft
size={14}
className="text-emerald-400"
/>
) : entry.status === "rejected" ? (
<X size={14} className="text-red-400" />
) : (
<Clock size={14} className="text-amber-400" />
)}
</div>
<div>
<p className="text-sm font-medium text-white">
{entry.amountFlh.toLocaleString()} FLH
</p>
<p className="text-[11px] text-gray-500">
${entry.fiatAmount.toFixed(2)} {" "}
{new Date(entry.createdAt).toLocaleDateString()}
</p>
</div>
</div>
<span
className={`text-xs font-medium capitalize ${
entry.status === "approved"
? "text-emerald-400"
: entry.status === "rejected"
? "text-red-400"
: "text-amber-400"
}`}
>
{entry.status}
</span>
</div>
))}
</div>
)}
</div>
{/* Earning Tips */}
<div className="bg-[#111118] border border-gray-800/60 rounded-2xl p-5">
<h3 className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
<Sparkles size={16} className="text-[#D4AF37]" />
Earn More FLH
</h3>
<ul className="space-y-2">
{EARNING_TIPS.map((tip, i) => (
<li key={i} className="flex items-start gap-2 text-sm text-gray-400">
<Info size={12} className="text-[#D4AF37] mt-0.5 shrink-0" />
<span>{tip}</span>
</li>
))}
</ul>
</div>
<div className="h-4" />
</div>
</div>
);
}