Mobile-first UI overhaul for Souq, Wallet, Forum pages
- Souq: bottom sheets, emoji category chips, skeleton loading, toast notifications - Wallet: gradient balance card with GBP preview, status badges, clean history - Forum: back nav, skeleton loading, bottom sheet create, thread author avatars - All: pb-24 clearance, rounded-2xl, active: press states, no hover: classes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+167
-49
@@ -2,73 +2,162 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useAuth } from '@/lib/AuthContext'
|
import { useAuth } from '@/lib/AuthContext'
|
||||||
import { Plus, ChevronRight } from 'lucide-react'
|
import { Plus, ChevronRight, MessageCircle, X, ArrowLeft } from 'lucide-react'
|
||||||
|
|
||||||
interface Category { id: string; name: string; description: string; icon: string }
|
interface Category { id: string; name: string; description: string; icon: string }
|
||||||
interface Thread { id: string; title: string; content: string; author: { name: string }; category: { name: string }; _count: { posts: number }; createdAt: string }
|
interface Thread {
|
||||||
|
id: string; title: string; content: string
|
||||||
|
author: { name: string }; category: { name: string }
|
||||||
|
_count: { posts: number }; createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function ForumPage() {
|
export default function ForumPage() {
|
||||||
const { token } = useAuth()
|
const { token } = useAuth()
|
||||||
const [categories, setCategories] = useState<Category[]>([])
|
const [categories, setCategories] = useState<Category[]>([])
|
||||||
const [threads, setThreads] = useState<Thread[]>([])
|
const [threads, setThreads] = useState<Thread[]>([])
|
||||||
const [selectedCat, setSelectedCat] = useState<string | null>(null)
|
const [selectedCat, setSelectedCat] = useState<Category | null>(null)
|
||||||
const [view, setView] = useState<'categories' | 'threads'>('categories')
|
const [view, setView] = useState<'categories' | 'threads'>('categories')
|
||||||
const [showCreate, setShowCreate] = useState(false)
|
const [showCreate, setShowCreate] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [toast, setToast] = useState('')
|
||||||
|
|
||||||
useEffect(() => { fetch('/api/forum/categories').then(r => r.json()).then(d => setCategories(d.categories || [])) }, [])
|
useEffect(() => {
|
||||||
|
fetch('/api/forum/categories')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => setCategories(d.categories || []))
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}, [])
|
||||||
|
|
||||||
const loadThreads = async (catId?: string) => {
|
const loadThreads = async (catId: string) => {
|
||||||
const params = catId ? `?categoryId=${catId}` : ''
|
setLoading(true)
|
||||||
const res = await fetch(`/api/forum/threads${params}`)
|
const res = await fetch(`/api/forum/threads?categoryId=${catId}`)
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
setThreads(data.threads || [])
|
setThreads(data.threads || [])
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showToast = (msg: string) => { setToast(msg); setTimeout(() => setToast(''), 3000) }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto p-4 sm:p-6 space-y-6">
|
<div className="min-h-dvh bg-[#0a0a0f] pb-24">
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h1 className="text-2xl font-bold text-[#D4AF37]">Forum</h1>
|
{/* Header */}
|
||||||
|
<div className="px-5 pt-6 pb-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{view === 'threads' && (
|
||||||
|
<button onClick={() => { setView('categories'); setSelectedCat(null) }} className="w-9 h-9 flex items-center justify-center rounded-xl bg-gray-800/60 active:opacity-60">
|
||||||
|
<ArrowLeft size={18} className="text-gray-400" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-bold text-white">{view === 'threads' && selectedCat ? selectedCat.name : 'Forum'}</h1>
|
||||||
|
<p className="text-[11px] text-gray-600 mt-0.5">
|
||||||
|
{view === 'categories' ? 'Community discussions' : `${threads.length} threads`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{token && (
|
{token && (
|
||||||
<button onClick={() => setShowCreate(true)}
|
<button
|
||||||
className="flex items-center gap-1 bg-[#D4AF37] text-[#0a0a0f] px-3 py-2 rounded text-sm font-semibold hover:bg-[#C9A84C] transition"><Plus size={16} /> New Thread</button>
|
onClick={() => setShowCreate(true)}
|
||||||
|
className="flex items-center gap-1.5 bg-[#D4AF37] text-[#0a0a0f] px-4 py-2.5 rounded-xl font-bold text-sm active:scale-[0.96] transition-transform"
|
||||||
|
>
|
||||||
|
<Plus size={16} /> Post
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{view === 'categories' ? (
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
{/* Loading */}
|
||||||
{categories.map(c => (
|
{loading && (
|
||||||
<button key={c.id} onClick={() => { setSelectedCat(c.id); setView('threads'); loadThreads(c.id) }}
|
<div className="px-5 space-y-3">
|
||||||
className="bg-[#111118] border border-gray-800 rounded-lg p-4 text-left hover:border-gray-700 transition">
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<div key={i} className="h-20 rounded-2xl bg-gray-900/60 animate-pulse" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Categories view */}
|
||||||
|
{!loading && view === 'categories' && (
|
||||||
|
<div className="px-5 space-y-3">
|
||||||
|
{categories.length === 0 ? (
|
||||||
|
<div className="text-center py-16">
|
||||||
|
<div className="text-4xl mb-3">🕌</div>
|
||||||
|
<p className="text-gray-500 text-sm">No categories yet</p>
|
||||||
|
</div>
|
||||||
|
) : categories.map(c => (
|
||||||
|
<button
|
||||||
|
key={c.id}
|
||||||
|
onClick={() => { setSelectedCat(c); setView('threads'); loadThreads(c.id) }}
|
||||||
|
className="w-full text-left bg-[#111118] border border-gray-800/60 rounded-2xl p-4 active:scale-[0.99] transition-transform"
|
||||||
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-2xl">{c.icon}</span>
|
<span className="text-2xl w-10 h-10 flex items-center justify-center bg-gray-800/60 rounded-xl shrink-0">
|
||||||
<div className="flex-1">
|
{c.icon || '💬'}
|
||||||
<h3 className="font-semibold">{c.name}</h3>
|
</span>
|
||||||
<p className="text-xs text-gray-500">{c.description}</p>
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="font-semibold text-white text-sm">{c.name}</h3>
|
||||||
|
<p className="text-xs text-gray-500 mt-0.5 line-clamp-1">{c.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight size={16} className="text-gray-600" />
|
<ChevronRight size={16} className="text-gray-700 shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
)}
|
||||||
<div className="space-y-3">
|
|
||||||
<button onClick={() => setView('categories')} className="text-sm text-[#D4AF37] hover:underline mb-2 inline-block">← Back to categories</button>
|
{/* Threads view */}
|
||||||
{threads.map(t => (
|
{!loading && view === 'threads' && (
|
||||||
<div key={t.id} className="bg-[#111118] border border-gray-800 rounded-lg p-4 space-y-1">
|
<div className="px-5 space-y-3">
|
||||||
<h3 className="font-semibold">{t.title}</h3>
|
{threads.length === 0 ? (
|
||||||
<p className="text-sm text-gray-400 line-clamp-2">{t.content}</p>
|
<div className="text-center py-16">
|
||||||
<div className="flex items-center gap-3 text-xs text-gray-500 pt-1">
|
<div className="text-4xl mb-3">💬</div>
|
||||||
<span>{t.author.name}</span><span>{t.category.name}</span><span>{t._count.posts} replies</span><span>{new Date(t.createdAt).toLocaleDateString()}</span>
|
<p className="text-white font-semibold mb-1">No threads yet</p>
|
||||||
|
<p className="text-gray-500 text-sm">Start the first discussion</p>
|
||||||
|
</div>
|
||||||
|
) : threads.map(t => (
|
||||||
|
<div key={t.id} className="bg-[#111118] border border-gray-800/60 rounded-2xl p-4">
|
||||||
|
<h3 className="font-semibold text-white text-sm leading-snug mb-1">{t.title}</h3>
|
||||||
|
<p className="text-xs text-gray-500 line-clamp-2 mb-3">{t.content}</p>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="w-5 h-5 rounded-full bg-[#D4AF37]/15 flex items-center justify-center">
|
||||||
|
<span className="text-[9px] text-[#D4AF37] font-bold">{t.author.name[0]}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] text-gray-600">{t.author.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 text-[11px] text-gray-700">
|
||||||
|
<MessageCircle size={12} />
|
||||||
|
{t._count.posts}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showCreate && token && <CreateThreadModal token={token} categories={categories} onClose={() => setShowCreate(false)} onCreated={() => { setShowCreate(false); if (selectedCat) loadThreads(selectedCat) }} />}
|
|
||||||
|
{/* Toast */}
|
||||||
|
{toast && (
|
||||||
|
<div className="fixed bottom-24 left-5 right-5 bg-[#1a1a26] border border-gray-700 rounded-2xl px-4 py-3 text-sm text-white text-center z-50 shadow-xl">
|
||||||
|
{toast}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Create thread bottom sheet */}
|
||||||
|
{showCreate && token && (
|
||||||
|
<CreateThreadSheet
|
||||||
|
token={token}
|
||||||
|
categories={categories}
|
||||||
|
onClose={() => setShowCreate(false)}
|
||||||
|
onCreated={() => { if (selectedCat) loadThreads(selectedCat.id); showToast('Thread posted!') }}
|
||||||
|
showToast={showToast}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CreateThreadModal({ token, categories, onClose, onCreated }: { token: string; categories: Category[]; onClose: () => void; onCreated: () => void }) {
|
function CreateThreadSheet({ token, categories, onClose, onCreated, showToast }: {
|
||||||
|
token: string; categories: Category[]; onClose: () => void; onCreated: () => void; showToast: (m: string) => void
|
||||||
|
}) {
|
||||||
const [title, setTitle] = useState('')
|
const [title, setTitle] = useState('')
|
||||||
const [content, setContent] = useState('')
|
const [content, setContent] = useState('')
|
||||||
const [categoryId, setCategoryId] = useState(categories[0]?.id || '')
|
const [categoryId, setCategoryId] = useState(categories[0]?.id || '')
|
||||||
@@ -76,29 +165,58 @@ function CreateThreadModal({ token, categories, onClose, onCreated }: { token: s
|
|||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
if (!categoryId) return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const res = await fetch('/api/forum/threads', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ title, content, categoryId }) })
|
try {
|
||||||
|
const res = await fetch('/api/forum/threads', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ title, content, categoryId }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (res.ok) { onCreated(); onClose() }
|
||||||
|
else showToast(data.error || 'Failed to post')
|
||||||
|
} catch { showToast('Something went wrong') }
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
if (res.ok) { onCreated() } else { const d = await res.json(); alert(d.error) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/70 flex items-center justify-center p-4 z-50">
|
<div className="fixed inset-0 z-50 flex flex-col justify-end" onClick={onClose}>
|
||||||
<form onSubmit={handleSubmit} className="bg-[#111118] border border-gray-800 rounded-lg p-6 max-w-md w-full space-y-4">
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||||
<h2 className="text-lg font-bold">New Thread</h2>
|
<form
|
||||||
<select value={categoryId} onChange={e => setCategoryId(e.target.value)}
|
onSubmit={handleSubmit}
|
||||||
className="w-full bg-[#0a0a0f] border border-gray-700 rounded px-3 py-2 text-sm focus:border-[#D4AF37] outline-none">
|
className="relative bg-[#0f0f18] border border-gray-800/60 rounded-t-3xl p-6 pb-10 space-y-4"
|
||||||
{categories.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<div className="w-10 h-1 bg-gray-700 rounded-full mx-auto absolute left-1/2 -translate-x-1/2 top-3" />
|
||||||
|
<h2 className="text-lg font-bold text-white mt-2">New Thread</h2>
|
||||||
|
<button type="button" onClick={onClose} className="w-8 h-8 flex items-center justify-center rounded-full bg-gray-800 mt-2">
|
||||||
|
<X size={15} className="text-gray-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={categoryId} onChange={e => setCategoryId(e.target.value)}
|
||||||
|
className="w-full bg-[#0a0a0f] border border-gray-800 rounded-2xl px-4 py-4 text-sm text-white focus:border-[#D4AF37]/50 focus:outline-none"
|
||||||
|
>
|
||||||
|
{categories.map(c => <option key={c.id} value={c.id}>{c.icon} {c.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<input type="text" placeholder="Title" value={title} onChange={e => setTitle(e.target.value)} required
|
|
||||||
className="w-full bg-[#0a0a0f] border border-gray-700 rounded px-3 py-2 text-sm focus:border-[#D4AF37] outline-none" />
|
<input
|
||||||
<textarea placeholder="Content" value={content} onChange={e => setContent(e.target.value)} required rows={4}
|
type="text" placeholder="Thread title" value={title} onChange={e => setTitle(e.target.value)} required
|
||||||
className="w-full bg-[#0a0a0f] border border-gray-700 rounded px-3 py-2 text-sm focus:border-[#D4AF37] outline-none" />
|
className="w-full bg-[#0a0a0f] border border-gray-800 rounded-2xl px-4 py-4 text-sm text-white placeholder-gray-600 focus:border-[#D4AF37]/50 focus:outline-none"
|
||||||
<button type="submit" disabled={loading}
|
/>
|
||||||
className="w-full bg-[#D4AF37] text-[#0a0a0f] py-2 rounded font-semibold disabled:opacity-50 hover:bg-[#C9A84C] transition">
|
<textarea
|
||||||
{loading ? 'Posting...' : 'Create Thread'}
|
placeholder="Share your thoughts…" value={content} onChange={e => setContent(e.target.value)} required rows={4}
|
||||||
|
className="w-full bg-[#0a0a0f] border border-gray-800 rounded-2xl px-4 py-3 text-sm text-white placeholder-gray-600 focus:border-[#D4AF37]/50 focus:outline-none resize-none"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit" disabled={loading}
|
||||||
|
className="w-full bg-[#D4AF37] text-[#0a0a0f] py-4 rounded-2xl font-bold disabled:opacity-50 active:scale-[0.98] transition-transform"
|
||||||
|
>
|
||||||
|
{loading ? 'Posting…' : 'Post Thread'}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={onClose} className="w-full text-gray-500 text-sm hover:text-white">Cancel</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
+226
-84
@@ -1,29 +1,47 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { useAuth } from '@/lib/AuthContext'
|
import { useAuth } from '@/lib/AuthContext'
|
||||||
import { Search, Plus, ShoppingCart, Zap } from 'lucide-react'
|
import { Search, Plus, ShoppingCart, Zap, X, ChevronRight, Package } from 'lucide-react'
|
||||||
|
|
||||||
interface Listing { id: string; sellerId: string; title: string; category: string; price_flh: number; seller: string; description: string; status: string; featured: boolean; featuredUntil: string | null; fileType: string | null; createdAt: string }
|
interface Listing {
|
||||||
|
id: string; sellerId: string; title: string; category: string
|
||||||
|
price_flh: number; seller: string; description: string; status: string
|
||||||
|
featured: boolean; featuredUntil: string | null; fileType: string | null; createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
const CATEGORIES = ['All', 'E-Books', 'Courses', 'Design', 'Audio', 'Video', 'Software', 'Other']
|
const CATEGORIES = ['All', 'E-Books', 'Courses', 'Design', 'Audio', 'Video', 'Software', 'Other']
|
||||||
|
|
||||||
|
const CATEGORY_ICONS: Record<string, string> = {
|
||||||
|
'All': '🏪', 'E-Books': '📚', 'Courses': '🎓', 'Design': '🎨',
|
||||||
|
'Audio': '🎵', 'Video': '🎬', 'Software': '💻', 'Other': '📦',
|
||||||
|
}
|
||||||
|
|
||||||
export default function SouqPage() {
|
export default function SouqPage() {
|
||||||
const { token, user, loading: authLoading } = useAuth()
|
const { token, user } = useAuth()
|
||||||
const [listings, setListings] = useState<Listing[]>([])
|
const [listings, setListings] = useState<Listing[]>([])
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [category, setCategory] = useState('All')
|
const [category, setCategory] = useState('All')
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [showCreate, setShowCreate] = useState(false)
|
const [showCreate, setShowCreate] = useState(false)
|
||||||
const [selectedListing, setSelectedListing] = useState<Listing | null>(null)
|
const [selectedListing, setSelectedListing] = useState<Listing | null>(null)
|
||||||
|
const [toast, setToast] = useState('')
|
||||||
|
|
||||||
const fetchListings = async () => {
|
const fetchListings = async () => {
|
||||||
try { const res = await fetch('/api/marketplace/listings'); const data = await res.json(); setListings(data.listings || []) }
|
try {
|
||||||
catch (e) { console.error(e) } finally { setLoading(false) }
|
const res = await fetch('/api/marketplace/listings')
|
||||||
|
const data = await res.json()
|
||||||
|
setListings(data.listings || [])
|
||||||
|
} catch { } finally { setLoading(false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => { fetchListings() }, [])
|
useEffect(() => { fetchListings() }, [])
|
||||||
|
|
||||||
|
const showToast = (msg: string) => {
|
||||||
|
setToast(msg)
|
||||||
|
setTimeout(() => setToast(''), 3000)
|
||||||
|
}
|
||||||
|
|
||||||
const filtered = listings.filter(l => {
|
const filtered = listings.filter(l => {
|
||||||
if (category !== 'All' && l.category !== category) return false
|
if (category !== 'All' && l.category !== category) return false
|
||||||
if (search && !l.title.toLowerCase().includes(search.toLowerCase())) return false
|
if (search && !l.title.toLowerCase().includes(search.toLowerCase())) return false
|
||||||
@@ -32,93 +50,190 @@ export default function SouqPage() {
|
|||||||
|
|
||||||
const handlePurchase = async (listingId: string) => {
|
const handlePurchase = async (listingId: string) => {
|
||||||
if (!token) return
|
if (!token) return
|
||||||
const res = await fetch('/api/marketplace/purchase', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ listingId }) })
|
const res = await fetch('/api/marketplace/purchase', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ listingId }),
|
||||||
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (res.ok) { fetchListings() } else { alert(data.error) }
|
if (res.ok) { fetchListings(); showToast('Purchase successful!'); setSelectedListing(null) }
|
||||||
|
else showToast(data.error || 'Purchase failed')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authLoading) return <div className="p-8 text-center text-gray-500">Loading...</div>
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto p-4 sm:p-6 space-y-6">
|
<div className="min-h-dvh bg-[#0a0a0f] pb-24 select-none">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
||||||
<h1 className="text-2xl font-bold text-[#D4AF37]">Souq</h1>
|
{/* Header */}
|
||||||
<div className="flex gap-2">
|
<div className="px-5 pt-6 pb-4">
|
||||||
<div className="relative flex-1 sm:flex-none">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" />
|
<div>
|
||||||
<input type="text" placeholder="Search..." value={search} onChange={e => setSearch(e.target.value)}
|
<h1 className="text-xl font-bold text-white">Souq</h1>
|
||||||
className="w-full sm:w-48 bg-[#111118] border border-gray-700 rounded pl-9 pr-3 py-2 text-sm focus:border-[#D4AF37] outline-none" />
|
<p className="text-[11px] text-gray-600 mt-0.5">Islamic digital marketplace</p>
|
||||||
</div>
|
</div>
|
||||||
{token && (
|
{token && (
|
||||||
<button onClick={() => setShowCreate(true)}
|
<button
|
||||||
className="flex items-center gap-1 bg-[#D4AF37] text-[#0a0a0f] px-3 py-2 rounded text-sm font-semibold hover:bg-[#C9A84C] transition">
|
onClick={() => setShowCreate(true)}
|
||||||
<Plus size={16} /> Create
|
className="flex items-center gap-1.5 bg-[#D4AF37] text-[#0a0a0f] px-4 py-2.5 rounded-xl font-bold text-sm active:scale-[0.96] transition-transform"
|
||||||
|
>
|
||||||
|
<Plus size={16} /> Sell
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search size={16} className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-600" />
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
placeholder="Search products…"
|
||||||
|
value={search}
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
className="w-full bg-[#111118] border border-gray-800 rounded-2xl pl-11 pr-4 py-3.5 text-sm text-white placeholder-gray-600 focus:border-[#D4AF37]/50 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 overflow-x-auto pb-2">
|
|
||||||
|
{/* Category chips */}
|
||||||
|
<div className="flex gap-2 px-5 pb-4 overflow-x-auto scrollbar-none" style={{ scrollbarWidth: 'none' }}>
|
||||||
{CATEGORIES.map(c => (
|
{CATEGORIES.map(c => (
|
||||||
<button key={c} onClick={() => setCategory(c)}
|
<button
|
||||||
className={`px-3 py-1 rounded-full text-xs whitespace-nowrap transition ${category === c ? 'bg-[#D4AF37] text-[#0a0a0f] font-semibold' : 'bg-gray-800 text-gray-400 hover:text-white'}`}>{c}</button>
|
key={c}
|
||||||
|
onClick={() => setCategory(c)}
|
||||||
|
className={`flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-semibold whitespace-nowrap transition-all active:scale-95 ${
|
||||||
|
category === c
|
||||||
|
? 'bg-[#D4AF37] text-[#0a0a0f]'
|
||||||
|
: 'bg-gray-900 text-gray-500 border border-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{CATEGORY_ICONS[c]}</span>
|
||||||
|
{c}
|
||||||
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{loading ? (
|
|
||||||
<div className="text-center text-gray-500 py-12">Loading listings...</div>
|
{/* Loading */}
|
||||||
) : filtered.length === 0 ? (
|
{loading && (
|
||||||
<div className="text-center text-gray-500 py-12">No listings found</div>
|
<div className="px-5 space-y-3">
|
||||||
) : (
|
{[...Array(4)].map((_, i) => (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div key={i} className="h-28 rounded-2xl bg-gray-900/60 animate-pulse" />
|
||||||
{filtered.map(l => (
|
|
||||||
<div key={l.id} className="bg-[#111118] border border-gray-800 rounded-lg p-4 space-y-2 hover:border-gray-700 transition cursor-pointer" onClick={() => setSelectedListing(l)}>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<span className="text-xs text-gray-500 bg-gray-800 px-2 py-0.5 rounded">{l.category}</span>
|
|
||||||
{l.featured && <Zap size={14} className="text-[#D4AF37]" />}
|
|
||||||
</div>
|
|
||||||
<h3 className="font-semibold">{l.title}</h3>
|
|
||||||
<p className="text-sm text-gray-400 line-clamp-2">{l.description}</p>
|
|
||||||
<div className="flex items-center justify-between pt-2">
|
|
||||||
<span className="text-[#D4AF37] font-bold">{l.price_flh.toLocaleString()} FLH</span>
|
|
||||||
<span className="text-xs text-gray-500">{l.seller}</span>
|
|
||||||
</div>
|
|
||||||
{token && l.sellerId !== user?.id && (
|
|
||||||
<button onClick={e => { e.stopPropagation(); handlePurchase(l.id) }}
|
|
||||||
className="w-full flex items-center justify-center gap-1 bg-[#D4AF37]/10 text-[#D4AF37] border border-[#D4AF37]/30 rounded py-1.5 text-sm hover:bg-[#D4AF37]/20 transition">
|
|
||||||
<ShoppingCart size={14} /> Buy
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Empty */}
|
||||||
|
{!loading && filtered.length === 0 && (
|
||||||
|
<div className="flex flex-col items-center justify-center pt-20 px-6 text-center">
|
||||||
|
<div className="text-5xl mb-4">🏪</div>
|
||||||
|
<p className="text-white font-semibold mb-1">Nothing here yet</p>
|
||||||
|
<p className="text-gray-600 text-sm">Be the first to list a product</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Listings */}
|
||||||
|
{!loading && filtered.length > 0 && (
|
||||||
|
<div className="px-5 space-y-3">
|
||||||
|
{filtered.map(l => (
|
||||||
|
<button
|
||||||
|
key={l.id}
|
||||||
|
onClick={() => setSelectedListing(l)}
|
||||||
|
className="w-full text-left bg-[#111118] border border-gray-800/60 rounded-2xl p-4 active:scale-[0.99] transition-transform"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
{/* Icon block */}
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-gray-800/60 flex items-center justify-center shrink-0 text-xl">
|
||||||
|
{CATEGORY_ICONS[l.category] || '📦'}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<h3 className="font-semibold text-white text-sm leading-tight line-clamp-1">{l.title}</h3>
|
||||||
|
{l.featured && <Zap size={13} className="text-[#D4AF37] shrink-0 mt-0.5" />}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-0.5 line-clamp-1">{l.description}</p>
|
||||||
|
<div className="flex items-center justify-between mt-2">
|
||||||
|
<span className="text-[#D4AF37] font-bold text-sm">{l.price_flh.toLocaleString()} FLH</span>
|
||||||
|
<span className="text-[11px] text-gray-700">{l.seller}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChevronRight size={16} className="text-gray-700 shrink-0 mt-1" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Toast */}
|
||||||
|
{toast && (
|
||||||
|
<div className="fixed bottom-24 left-5 right-5 bg-[#1a1a26] border border-gray-700 rounded-2xl px-4 py-3 text-sm text-white text-center z-50 shadow-xl">
|
||||||
|
{toast}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Detail bottom sheet */}
|
||||||
{selectedListing && (
|
{selectedListing && (
|
||||||
<div className="fixed inset-0 bg-black/70 flex items-center justify-center p-4 z-50" onClick={() => setSelectedListing(null)}>
|
<div className="fixed inset-0 z-50 flex flex-col justify-end" onClick={() => setSelectedListing(null)}>
|
||||||
<div className="bg-[#111118] border border-gray-800 rounded-lg p-6 max-w-lg w-full space-y-4" onClick={e => e.stopPropagation()}>
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||||
<div className="flex items-start justify-between">
|
<div
|
||||||
<h2 className="text-xl font-bold">{selectedListing.title}</h2>
|
className="relative bg-[#0f0f18] border border-gray-800/60 rounded-t-3xl p-6 pb-10 max-h-[80dvh] overflow-y-auto"
|
||||||
<button onClick={() => setSelectedListing(null)} className="text-gray-500 hover:text-white">✕</button>
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Handle */}
|
||||||
|
<div className="w-10 h-1 bg-gray-700 rounded-full mx-auto mb-5" />
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3 mb-4">
|
||||||
|
<div className="w-14 h-14 rounded-2xl bg-gray-800/60 flex items-center justify-center text-2xl shrink-0">
|
||||||
|
{CATEGORY_ICONS[selectedListing.category] || '📦'}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h2 className="text-lg font-bold text-white leading-tight">{selectedListing.title}</h2>
|
||||||
|
<p className="text-xs text-gray-500 mt-0.5">by {selectedListing.seller}</p>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => setSelectedListing(null)} className="w-8 h-8 flex items-center justify-center rounded-full bg-gray-800">
|
||||||
|
<X size={15} className="text-gray-400" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-400">{selectedListing.description}</p>
|
|
||||||
<div className="flex gap-2 text-sm">
|
<span className="inline-block text-[11px] bg-gray-800 text-gray-400 px-3 py-1 rounded-full mb-3">{selectedListing.category}</span>
|
||||||
<span className="bg-gray-800 px-2 py-0.5 rounded">{selectedListing.category}</span>
|
<p className="text-sm text-gray-400 leading-relaxed mb-5">{selectedListing.description}</p>
|
||||||
<span className="text-gray-500">by {selectedListing.seller}</span>
|
|
||||||
|
<div className="flex items-center justify-between mb-5">
|
||||||
|
<p className="text-2xl font-bold text-[#D4AF37]">{selectedListing.price_flh.toLocaleString()} FLH</p>
|
||||||
|
{user && <p className="text-xs text-gray-600">Your balance: {user.flhBalance?.toLocaleString() || 0} FLH</p>}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-2xl font-bold text-[#D4AF37]">{selectedListing.price_flh.toLocaleString()} FLH</p>
|
|
||||||
{token && selectedListing.sellerId !== user?.id && (
|
{token && selectedListing.sellerId !== user?.id ? (
|
||||||
<button onClick={() => { handlePurchase(selectedListing.id); setSelectedListing(null) }}
|
<button
|
||||||
className="w-full bg-[#D4AF37] text-[#0a0a0f] py-2 rounded font-semibold hover:bg-[#C9A84C] transition">
|
onClick={() => handlePurchase(selectedListing.id)}
|
||||||
Purchase
|
className="w-full flex items-center justify-center gap-2 bg-[#D4AF37] text-[#0a0a0f] py-4 rounded-2xl font-bold text-base active:scale-[0.98] transition-transform"
|
||||||
|
>
|
||||||
|
<ShoppingCart size={18} /> Purchase Now
|
||||||
|
</button>
|
||||||
|
) : selectedListing.sellerId === user?.id ? (
|
||||||
|
<div className="w-full text-center py-4 rounded-2xl bg-gray-900 text-gray-500 text-sm">Your listing</div>
|
||||||
|
) : (
|
||||||
|
<button onClick={() => setSelectedListing(null)} className="w-full text-center py-4 rounded-2xl bg-gray-900 text-gray-400 text-sm">
|
||||||
|
Sign in to purchase
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showCreate && token && <CreateListingModal token={token} userId={user!.id} onClose={() => setShowCreate(false)} onCreated={fetchListings} />}
|
|
||||||
|
{/* Create listing bottom sheet */}
|
||||||
|
{showCreate && token && (
|
||||||
|
<CreateListingSheet
|
||||||
|
token={token}
|
||||||
|
onClose={() => setShowCreate(false)}
|
||||||
|
onCreated={fetchListings}
|
||||||
|
showToast={showToast}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CreateListingModal({ token, onClose, onCreated }: { token: string; userId: string; onClose: () => void; onCreated: () => void }) {
|
function CreateListingSheet({ token, onClose, onCreated, showToast }: {
|
||||||
|
token: string; onClose: () => void; onCreated: () => void; showToast: (m: string) => void
|
||||||
|
}) {
|
||||||
const [title, setTitle] = useState('')
|
const [title, setTitle] = useState('')
|
||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState('')
|
||||||
const [category, setCategory] = useState('E-Books')
|
const [category, setCategory] = useState('E-Books')
|
||||||
@@ -128,31 +243,58 @@ function CreateListingModal({ token, onClose, onCreated }: { token: string; user
|
|||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const res = await fetch('/api/marketplace/listings', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ title, description, category, priceFlh: parseInt(priceFlh) }) })
|
try {
|
||||||
const data = await res.json()
|
const res = await fetch('/api/marketplace/listings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ title, description, category, priceFlh: parseInt(priceFlh) }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (res.ok) { onCreated(); onClose(); showToast('Listing created!') }
|
||||||
|
else showToast(data.error || 'Failed to create')
|
||||||
|
} catch { showToast('Something went wrong') }
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
if (res.ok) { onCreated(); onClose() } else { alert(data.error) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/70 flex items-center justify-center p-4 z-50">
|
<div className="fixed inset-0 z-50 flex flex-col justify-end" onClick={onClose}>
|
||||||
<form onSubmit={handleSubmit} className="bg-[#111118] border border-gray-800 rounded-lg p-6 max-w-md w-full space-y-4">
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||||
<h2 className="text-lg font-bold">Create Listing</h2>
|
<form
|
||||||
<input type="text" placeholder="Title" value={title} onChange={e => setTitle(e.target.value)} required
|
onSubmit={handleSubmit}
|
||||||
className="w-full bg-[#0a0a0f] border border-gray-700 rounded px-3 py-2 text-sm focus:border-[#D4AF37] outline-none" />
|
className="relative bg-[#0f0f18] border border-gray-800/60 rounded-t-3xl p-6 pb-10 space-y-4"
|
||||||
<textarea placeholder="Description" value={description} onChange={e => setDescription(e.target.value)} required rows={3}
|
onClick={e => e.stopPropagation()}
|
||||||
className="w-full bg-[#0a0a0f] border border-gray-700 rounded px-3 py-2 text-sm focus:border-[#D4AF37] outline-none" />
|
>
|
||||||
<select value={category} onChange={e => setCategory(e.target.value)}
|
<div className="w-10 h-1 bg-gray-700 rounded-full mx-auto mb-2" />
|
||||||
className="w-full bg-[#0a0a0f] border border-gray-700 rounded px-3 py-2 text-sm focus:border-[#D4AF37] outline-none">
|
<h2 className="text-lg font-bold text-white">New Listing</h2>
|
||||||
{['E-Books', 'Courses', 'Design', 'Audio', 'Video', 'Software', 'Other'].map(c => <option key={c} value={c}>{c}</option>)}
|
|
||||||
|
<input
|
||||||
|
type="text" placeholder="Product title" value={title} onChange={e => setTitle(e.target.value)} required
|
||||||
|
className="w-full bg-[#0a0a0f] border border-gray-800 rounded-2xl px-4 py-4 text-sm text-white placeholder-gray-600 focus:border-[#D4AF37]/50 focus:outline-none"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
placeholder="Description" value={description} onChange={e => setDescription(e.target.value)} required rows={3}
|
||||||
|
className="w-full bg-[#0a0a0f] border border-gray-800 rounded-2xl px-4 py-3 text-sm text-white placeholder-gray-600 focus:border-[#D4AF37]/50 focus:outline-none resize-none"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
value={category} onChange={e => setCategory(e.target.value)}
|
||||||
|
className="w-full bg-[#0a0a0f] border border-gray-800 rounded-2xl px-4 py-4 text-sm text-white focus:border-[#D4AF37]/50 focus:outline-none"
|
||||||
|
>
|
||||||
|
{['E-Books', 'Courses', 'Design', 'Audio', 'Video', 'Software', 'Other'].map(c => <option key={c}>{c}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<input type="number" placeholder="Price (FLH)" value={priceFlh} onChange={e => setPriceFlh(e.target.value)} required min={1}
|
<input
|
||||||
className="w-full bg-[#0a0a0f] border border-gray-700 rounded px-3 py-2 text-sm focus:border-[#D4AF37] outline-none" />
|
type="number" placeholder="Price in FLH" value={priceFlh} onChange={e => setPriceFlh(e.target.value)} required min={1}
|
||||||
<button type="submit" disabled={loading}
|
inputMode="numeric"
|
||||||
className="w-full bg-[#D4AF37] text-[#0a0a0f] py-2 rounded font-semibold disabled:opacity-50 hover:bg-[#C9A84C] transition">
|
className="w-full bg-[#0a0a0f] border border-gray-800 rounded-2xl px-4 py-4 text-sm text-white placeholder-gray-600 focus:border-[#D4AF37]/50 focus:outline-none"
|
||||||
{loading ? 'Creating...' : 'Create Listing'}
|
/>
|
||||||
|
<button
|
||||||
|
type="submit" disabled={loading}
|
||||||
|
className="w-full bg-[#D4AF37] text-[#0a0a0f] py-4 rounded-2xl font-bold disabled:opacity-50 active:scale-[0.98] transition-transform"
|
||||||
|
>
|
||||||
|
{loading ? 'Creating…' : 'Create Listing'}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={onClose} className="w-full text-gray-600 text-sm py-2">
|
||||||
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={onClose} className="w-full text-gray-500 text-sm hover:text-white">Cancel</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
+120
-31
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useAuth } from '@/lib/AuthContext'
|
import { useAuth } from '@/lib/AuthContext'
|
||||||
import { ArrowUpRight, History } from 'lucide-react'
|
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { ArrowDownLeft, Clock, CheckCircle2, XCircle, Wallet } from 'lucide-react'
|
||||||
|
|
||||||
export default function WalletPage() {
|
export default function WalletPage() {
|
||||||
const { token, user, loading: authLoading } = useAuth()
|
const { token, user, loading: authLoading } = useAuth()
|
||||||
@@ -11,54 +11,143 @@ export default function WalletPage() {
|
|||||||
const [amount, setAmount] = useState('')
|
const [amount, setAmount] = useState('')
|
||||||
const [cashouts, setCashouts] = useState<any[]>([])
|
const [cashouts, setCashouts] = useState<any[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [toast, setToast] = useState('')
|
||||||
|
|
||||||
useEffect(() => { if (!authLoading && !token) router.push('/login') }, [token, authLoading, router])
|
useEffect(() => { if (!authLoading && !token) router.push('/login') }, [token, authLoading, router])
|
||||||
useEffect(() => { if (!token) return; fetch('/api/wallet', { headers: { 'Authorization': `Bearer ${token}` } }).then(r => r.json()).then(d => setCashouts(d.requests || [])).catch(() => {}) }, [token])
|
useEffect(() => {
|
||||||
|
if (!token) return
|
||||||
|
fetch('/api/wallet', { headers: { Authorization: `Bearer ${token}` } })
|
||||||
|
.then(r => r.json()).then(d => setCashouts(d.requests || [])).catch(() => { })
|
||||||
|
}, [token])
|
||||||
|
|
||||||
|
const showToast = (msg: string) => { setToast(msg); setTimeout(() => setToast(''), 3000) }
|
||||||
|
|
||||||
const handleCashout = async () => {
|
const handleCashout = async () => {
|
||||||
if (!token || !amount) return
|
if (!token || !amount || parseInt(amount) < 100) return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const res = await fetch('/api/wallet', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ amountFlh: parseInt(amount) }) })
|
const res = await fetch('/api/wallet', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ amountFlh: parseInt(amount) }),
|
||||||
|
})
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (res.ok) { setAmount(''); alert('Cashout submitted!') } else { alert(data.error) }
|
if (res.ok) { setAmount(''); showToast('Cashout request submitted!') }
|
||||||
|
else showToast(data.error || 'Something went wrong')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authLoading) return <div className="p-8 text-center text-gray-500">Loading...</div>
|
if (authLoading) return (
|
||||||
|
<div className="min-h-dvh flex items-center justify-center bg-[#0a0a0f] pb-20">
|
||||||
|
<div className="w-8 h-8 rounded-full border-2 border-[#D4AF37]/30 border-t-[#D4AF37] animate-spin" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
if (!token) return null
|
if (!token) return null
|
||||||
|
|
||||||
|
const fiatValue = amount ? ((parseInt(amount) || 0) * 0.008).toFixed(2) : '0.00'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-3xl mx-auto p-4 sm:p-6 space-y-6">
|
<div className="min-h-dvh bg-[#0a0a0f] pb-24">
|
||||||
<h1 className="text-2xl font-bold text-[#D4AF37]">Wallet</h1>
|
|
||||||
<div className="bg-gradient-to-r from-[#D4AF37]/10 to-[#0a0a0f] border border-[#D4AF37]/20 rounded-lg p-6">
|
{/* Header */}
|
||||||
<p className="text-sm text-gray-400 mb-1">Balance</p>
|
<div className="px-5 pt-6 pb-5">
|
||||||
<p className="text-4xl font-bold text-[#D4AF37]">{user?.flhBalance?.toLocaleString() || 0} <span className="text-lg text-gray-500">FLH</span></p>
|
<h1 className="text-xl font-bold text-white">Wallet</h1>
|
||||||
|
<p className="text-[11px] text-gray-600 mt-0.5">Manage your FLH balance</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-[#111118] border border-gray-800 rounded-lg p-6 space-y-4">
|
|
||||||
<h2 className="font-semibold flex items-center gap-2"><ArrowUpRight size={16} className="text-[#D4AF37]" /> Cash Out</h2>
|
{/* Balance card */}
|
||||||
<p className="text-xs text-gray-500">Rate: 100 FLH = £0.80 (20% platform spread). Minimum 100 FLH.</p>
|
<div className="mx-5 mb-5 rounded-3xl overflow-hidden" style={{ background: 'linear-gradient(135deg, #1a1508 0%, #0f0f18 100%)', border: '1px solid rgba(212,175,55,0.2)' }}>
|
||||||
<div className="flex gap-2">
|
<div className="px-6 pt-6 pb-5">
|
||||||
<input type="number" placeholder="Amount (FLH)" value={amount} onChange={e => setAmount(e.target.value)} min={100}
|
<div className="flex items-center gap-2 mb-4">
|
||||||
className="flex-1 bg-[#0a0a0f] border border-gray-700 rounded px-3 py-2 text-sm focus:border-[#D4AF37] outline-none" />
|
<Wallet size={16} className="text-[#D4AF37]" />
|
||||||
<button onClick={handleCashout} disabled={loading || !amount || parseInt(amount) < 100}
|
<span className="text-xs text-[#D4AF37]/70 font-medium uppercase tracking-wider">FLH Balance</span>
|
||||||
className="bg-[#D4AF37] text-[#0a0a0f] px-4 py-2 rounded text-sm font-semibold disabled:opacity-50 hover:bg-[#C9A84C] transition">
|
</div>
|
||||||
{loading ? 'Processing...' : 'Cash Out'}
|
<p className="text-5xl font-bold text-white tabular-nums leading-none">
|
||||||
</button>
|
{(user?.flhBalance || 0).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
<p className="text-[#D4AF37] font-semibold mt-1 text-base">FLH Tokens</p>
|
||||||
|
<p className="text-xs text-gray-600 mt-3">
|
||||||
|
≈ £{((user?.flhBalance || 0) * 0.008).toFixed(2)} GBP at current rate
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-[#111118] border border-gray-800 rounded-lg p-6 space-y-3">
|
|
||||||
<h2 className="font-semibold flex items-center gap-2"><History size={16} className="text-[#D4AF37]" /> History</h2>
|
{/* Cash Out */}
|
||||||
{cashouts.length === 0 ? (
|
<div className="mx-5 mb-5 bg-[#111118] border border-gray-800/60 rounded-3xl p-5">
|
||||||
<p className="text-sm text-gray-500">No cashout requests yet</p>
|
<div className="flex items-center gap-2 mb-1">
|
||||||
) : (
|
<ArrowDownLeft size={16} className="text-[#D4AF37]" />
|
||||||
cashouts.map(c => (
|
<h2 className="font-bold text-white text-sm">Cash Out</h2>
|
||||||
<div key={c.id} className="flex items-center justify-between text-sm">
|
</div>
|
||||||
<span className="text-gray-400">{c.amountFlh} FLH → £{c.fiatAmount.toFixed(2)}</span>
|
<p className="text-[11px] text-gray-600 mb-4">100 FLH = £0.80 · Minimum 100 FLH</p>
|
||||||
<span className={`text-xs ${c.status === 'pending' ? 'text-yellow-400' : 'text-green-400'}`}>{c.status}</span>
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Amount in FLH"
|
||||||
|
value={amount}
|
||||||
|
onChange={e => setAmount(e.target.value)}
|
||||||
|
min={100}
|
||||||
|
inputMode="numeric"
|
||||||
|
className="w-full bg-[#0d0d14] border border-gray-800 rounded-2xl px-4 py-4 text-white placeholder-gray-700 text-sm focus:border-[#D4AF37]/50 focus:outline-none"
|
||||||
|
/>
|
||||||
|
{amount && parseInt(amount) > 0 && (
|
||||||
|
<div className="flex items-center justify-between px-1">
|
||||||
|
<span className="text-xs text-gray-600">{parseInt(amount).toLocaleString()} FLH</span>
|
||||||
|
<span className="text-xs text-[#D4AF37] font-semibold">£{fiatValue} GBP</span>
|
||||||
</div>
|
</div>
|
||||||
))
|
)}
|
||||||
|
<button
|
||||||
|
onClick={handleCashout}
|
||||||
|
disabled={loading || !amount || parseInt(amount) < 100}
|
||||||
|
className="w-full bg-[#D4AF37] text-[#0a0a0f] py-4 rounded-2xl font-bold text-sm disabled:opacity-40 active:scale-[0.98] transition-all"
|
||||||
|
>
|
||||||
|
{loading ? 'Submitting…' : 'Request Cash Out'}
|
||||||
|
</button>
|
||||||
|
{amount && parseInt(amount) < 100 && (
|
||||||
|
<p className="text-xs text-red-400 text-center">Minimum 100 FLH</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* History */}
|
||||||
|
<div className="mx-5 bg-[#111118] border border-gray-800/60 rounded-3xl p-5">
|
||||||
|
<h2 className="font-bold text-white text-sm mb-4">History</h2>
|
||||||
|
{cashouts.length === 0 ? (
|
||||||
|
<div className="text-center py-6">
|
||||||
|
<p className="text-gray-600 text-sm">No cashout requests yet</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{cashouts.map(c => (
|
||||||
|
<div key={c.id} className="flex items-center gap-3 py-2">
|
||||||
|
<div className="w-9 h-9 rounded-full bg-gray-800 flex items-center justify-center shrink-0">
|
||||||
|
{c.status === 'pending'
|
||||||
|
? <Clock size={16} className="text-yellow-400" />
|
||||||
|
: c.status === 'approved'
|
||||||
|
? <CheckCircle2 size={16} className="text-emerald-400" />
|
||||||
|
: <XCircle size={16} className="text-red-400" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm text-white font-medium">{c.amountFlh.toLocaleString()} FLH</p>
|
||||||
|
<p className="text-xs text-gray-600">£{c.fiatAmount.toFixed(2)} GBP</p>
|
||||||
|
</div>
|
||||||
|
<span className={`text-xs font-semibold capitalize px-2.5 py-1 rounded-full ${
|
||||||
|
c.status === 'pending' ? 'bg-yellow-400/10 text-yellow-400'
|
||||||
|
: c.status === 'approved' ? 'bg-emerald-400/10 text-emerald-400'
|
||||||
|
: 'bg-red-400/10 text-red-400'
|
||||||
|
}`}>{c.status}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Toast */}
|
||||||
|
{toast && (
|
||||||
|
<div className="fixed bottom-24 left-5 right-5 bg-[#1a1a26] border border-gray-700 rounded-2xl px-4 py-3 text-sm text-white text-center z-50 shadow-xl">
|
||||||
|
{toast}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user