fix(mobile): comprehensive mobile UI redesign for better UX

- Header: icon-only wallet button on mobile, fix hamburger overflow
- Dashboard: responsive tabs with flex-wrap and smaller text
- Citizens: responsive ID card layout, stack NFT badges on mobile
- BeCitizen: stack header buttons vertically on mobile
- GovernanceInterface: add scrollable tabs
- DEXDashboard: responsive admin buttons grid (2 cols on mobile)
- P2PDashboard: responsive header and scrollable tabs
- Dialog: mobile-first sizing with proper padding and max-height
- Tabs UI: base responsive classes for all tab components
- Add xs:480px breakpoint and scrollbar-hide utility
- Fix vite polyfills config to prevent initialization errors
This commit is contained in:
2026-02-11 02:14:32 +03:00
parent b173810c27
commit 12874822fc
13 changed files with 81 additions and 52 deletions
+2 -2
View File
@@ -72,11 +72,11 @@ const AppLayout: React.FC = () => {
</div> </div>
{/* Mobile: Wallet + Hamburger */} {/* Mobile: Wallet + Hamburger */}
<div className="flex lg:hidden items-center gap-2 w-full justify-end"> <div className="flex lg:hidden items-center gap-2 ml-auto">
<PezkuwiWalletButton /> <PezkuwiWalletButton />
<button <button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)} onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="p-2 rounded-lg text-gray-400 hover:text-white hover:bg-gray-800 transition-colors" className="p-2 rounded-lg text-gray-400 hover:text-white hover:bg-gray-800 transition-colors flex-shrink-0"
> >
{mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />} {mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button> </button>
+1 -1
View File
@@ -26,7 +26,7 @@ const GovernanceInterface: React.FC = () => {
</div> </div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full"> <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid grid-cols-3 lg:grid-cols-6 gap-2 bg-gray-900/50 p-1 rounded-lg"> <TabsList className="grid grid-cols-3 lg:grid-cols-6 gap-2 bg-gray-900/50 p-1 rounded-lg overflow-x-auto scrollbar-hide">
<TabsTrigger value="overview" className="flex items-center space-x-2"> <TabsTrigger value="overview" className="flex items-center space-x-2">
<TrendingUp className="w-4 h-4" /> <TrendingUp className="w-4 h-4" />
<span>Overview</span> <span>Overview</span>
+15 -7
View File
@@ -11,6 +11,7 @@ import {
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { Wallet, Check, ExternalLink, Copy, LogOut } from 'lucide-react'; import { Wallet, Check, ExternalLink, Copy, LogOut } from 'lucide-react';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { useIsMobile } from '@/hooks/use-mobile';
export const PezkuwiWalletButton: React.FC = () => { export const PezkuwiWalletButton: React.FC = () => {
const { const {
@@ -24,6 +25,7 @@ export const PezkuwiWalletButton: React.FC = () => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { toast } = useToast(); const { toast } = useToast();
const isMobile = useIsMobile();
const handleConnect = async () => { const handleConnect = async () => {
await connectWallet(); await connectWallet();
@@ -70,12 +72,17 @@ export const PezkuwiWalletButton: React.FC = () => {
variant="outline" variant="outline"
className="bg-green-500/20 border-green-500/50 text-green-400 hover:bg-green-500/30" className="bg-green-500/20 border-green-500/50 text-green-400 hover:bg-green-500/30"
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
size={isMobile ? "icon" : "default"}
> >
<Wallet className="w-4 h-4 mr-2" /> <Wallet className={isMobile ? "w-4 h-4" : "w-4 h-4 mr-2"} />
{selectedAccount.meta.name || 'Account'} {!isMobile && (
<Badge className="ml-2 bg-green-500/30 text-green-300 border-0"> <>
{formatAddress(selectedAccount.address)} {selectedAccount.meta.name || 'Account'}
</Badge> <Badge className="ml-2 bg-green-500/30 text-green-300 border-0">
{formatAddress(selectedAccount.address)}
</Badge>
</>
)}
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
@@ -169,9 +176,10 @@ export const PezkuwiWalletButton: React.FC = () => {
<Button <Button
onClick={handleConnect} onClick={handleConnect}
className="bg-gradient-to-r from-green-600 to-yellow-400 hover:from-green-700 hover:to-yellow-500 text-white" className="bg-gradient-to-r from-green-600 to-yellow-400 hover:from-green-700 hover:to-yellow-500 text-white"
size={isMobile ? "icon" : "default"}
> >
<Wallet className="w-4 h-4 mr-2" /> <Wallet className={isMobile ? "w-4 h-4" : "w-4 h-4 mr-2"} />
Connect Wallet {!isMobile && "Connect Wallet"}
</Button> </Button>
{error && error.includes('not found') && ( {error && error.includes('not found') && (
+1 -1
View File
@@ -137,7 +137,7 @@ export const DEXDashboard: React.FC = () => {
<p className="text-gray-400 mb-6"> <p className="text-gray-400 mb-6">
Mint wrapped tokens for testing and liquidity provision Mint wrapped tokens for testing and liquidity provision
</p> </p>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<button <button
onClick={() => setShowInitializeUsdtModal(true)} onClick={() => setShowInitializeUsdtModal(true)}
className="flex items-center justify-center gap-2 px-4 py-3 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors font-medium" className="flex items-center justify-center gap-2 px-4 py-3 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors font-medium"
+14 -14
View File
@@ -82,7 +82,7 @@ export function P2PDashboard() {
return ( return (
<div className="container mx-auto px-4 py-8 max-w-7xl"> <div className="container mx-auto px-4 py-8 max-w-7xl">
<div className="flex items-center justify-between mb-4"> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 mb-4">
<Button <Button
variant="ghost" variant="ghost"
onClick={() => navigate('/')} onClick={() => navigate('/')}
@@ -91,7 +91,7 @@ export function P2PDashboard() {
<Home className="w-4 h-4 mr-2" /> <Home className="w-4 h-4 mr-2" />
Back to Home Back to Home
</Button> </Button>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 flex-wrap">
<NotificationBell /> <NotificationBell />
<Button <Button
variant="outline" variant="outline"
@@ -168,12 +168,12 @@ export function P2PDashboard() {
</div> </div>
)} )}
<div className="flex items-center justify-between mb-8"> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-8">
<div> <div>
<h1 className="text-4xl font-bold text-white">P2P Trading</h1> <h1 className="text-3xl sm:text-4xl font-bold text-white">P2P Trading</h1>
<p className="text-gray-400">Buy and sell crypto with your local currency.</p> <p className="text-gray-400 text-sm sm:text-base">Buy and sell crypto with your local currency.</p>
</div> </div>
<Button onClick={() => setShowCreateAd(true)}> <Button onClick={() => setShowCreateAd(true)} className="w-full sm:w-auto">
<PlusCircle className="w-4 h-4 mr-2" /> <PlusCircle className="w-4 h-4 mr-2" />
Post a New Ad Post a New Ad
</Button> </Button>
@@ -187,17 +187,17 @@ export function P2PDashboard() {
<QuickFilterBar filters={filters} onFiltersChange={setFilters} /> <QuickFilterBar filters={filters} onFiltersChange={setFilters} />
<Tabs defaultValue="buy"> <Tabs defaultValue="buy">
<TabsList className="grid w-full grid-cols-5"> <TabsList className="grid w-full grid-cols-5 overflow-x-auto scrollbar-hide">
<TabsTrigger value="express" className="flex items-center gap-1"> <TabsTrigger value="express" className="flex items-center gap-1 text-xs sm:text-sm px-1 sm:px-3">
<Zap className="w-3 h-3" /> <Zap className="w-3 h-3" />
Express <span className="hidden xs:inline">Express</span>
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="buy">Buy</TabsTrigger> <TabsTrigger value="buy" className="text-xs sm:text-sm px-1 sm:px-3">Buy</TabsTrigger>
<TabsTrigger value="sell">Sell</TabsTrigger> <TabsTrigger value="sell" className="text-xs sm:text-sm px-1 sm:px-3">Sell</TabsTrigger>
<TabsTrigger value="my-ads">My Ads</TabsTrigger> <TabsTrigger value="my-ads" className="text-xs sm:text-sm px-1 sm:px-3">My Ads</TabsTrigger>
<TabsTrigger value="otc" className="flex items-center gap-1"> <TabsTrigger value="otc" className="flex items-center gap-1 text-xs sm:text-sm px-1 sm:px-3">
<Blocks className="w-3 h-3" /> <Blocks className="w-3 h-3" />
OTC <span className="hidden xs:inline">OTC</span>
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="express"> <TabsContent value="express">
+1 -1
View File
@@ -36,7 +36,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-border/40 bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", "fixed left-[50%] top-[50%] z-50 grid w-[calc(100vw-2rem)] sm:w-full max-w-lg max-h-[90vh] overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-4 border border-border/40 bg-background p-4 sm:p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg sm:rounded-lg",
className className
)} )}
{...props} {...props}
+2 -2
View File
@@ -12,7 +12,7 @@ const TabsList = React.forwardRef<
<TabsPrimitive.List <TabsPrimitive.List
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted/50 p-1 text-muted-foreground", "inline-flex h-auto min-h-10 items-center justify-center rounded-md bg-muted/50 p-1 text-muted-foreground flex-wrap gap-1",
className className
)} )}
{...props} {...props}
@@ -27,7 +27,7 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-primary data-[state=active]:shadow-sm hover:text-foreground", "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-primary data-[state=active]:shadow-sm hover:text-foreground",
className className
)} )}
{...props} {...props}
+10
View File
@@ -121,3 +121,13 @@
.markdown-preview ol { .markdown-preview ol {
@apply my-4 ml-6; @apply my-4 ml-6;
} }
/* Scrollbar hide utility */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
+3 -3
View File
@@ -27,11 +27,11 @@ const BeCitizen: React.FC = () => {
<div className="min-h-screen bg-gradient-to-br from-green-700 via-white to-red-600"> <div className="min-h-screen bg-gradient-to-br from-green-700 via-white to-red-600">
<div className="container mx-auto px-4 py-16"> <div className="container mx-auto px-4 py-16">
{/* Back to Home Button and Invite Friend */} {/* Back to Home Button and Invite Friend */}
<div className="mb-8 flex justify-between items-center"> <div className="mb-8 flex flex-col sm:flex-row gap-3 sm:justify-between sm:items-center">
<Button <Button
onClick={() => navigate('/')} onClick={() => navigate('/')}
variant="outline" variant="outline"
className="bg-red-600 hover:bg-red-700 border-yellow-400 border-2 text-white font-semibold shadow-lg" className="w-full sm:w-auto bg-red-600 hover:bg-red-700 border-yellow-400 border-2 text-white font-semibold shadow-lg"
> >
<ArrowLeft className="mr-2 h-4 w-4" /> <ArrowLeft className="mr-2 h-4 w-4" />
Back to Home Back to Home
@@ -39,7 +39,7 @@ const BeCitizen: React.FC = () => {
<Button <Button
onClick={() => setIsInviteModalOpen(true)} onClick={() => setIsInviteModalOpen(true)}
className="bg-green-600 hover:bg-green-700 border-yellow-400 border-2 text-white font-semibold shadow-lg" className="w-full sm:w-auto bg-green-600 hover:bg-green-700 border-yellow-400 border-2 text-white font-semibold shadow-lg"
> >
<UserPlus className="mr-2 h-4 w-4" /> <UserPlus className="mr-2 h-4 w-4" />
Invite Friend Invite Friend
+10 -10
View File
@@ -470,7 +470,7 @@ export default function Citizens() {
</div> </div>
{/* Main Content Area */} {/* Main Content Area */}
<div className="absolute top-16 bottom-10 left-0 right-0 px-5 flex"> <div className="absolute top-16 bottom-10 left-0 right-0 px-5 flex flex-col sm:flex-row">
{/* Left Section - Photo */} {/* Left Section - Photo */}
<div className="w-1/4 flex flex-col items-center justify-center"> <div className="w-1/4 flex flex-col items-center justify-center">
@@ -550,20 +550,20 @@ export default function Citizens() {
</div> </div>
{/* Right Section - NFT & ID Numbers */} {/* Right Section - NFT & ID Numbers */}
<div className="w-1/4 flex flex-col justify-center items-center space-y-2"> <div className="w-full sm:w-1/4 flex flex-row sm:flex-col justify-center items-center gap-2 sm:space-y-2 mt-2 sm:mt-0">
{/* NFT Badge */} {/* NFT Badge */}
<div className="bg-gradient-to-br from-green-700 to-green-900 rounded-xl p-3 text-center shadow-lg w-full max-w-[110px]"> <div className="bg-gradient-to-br from-green-700 to-green-900 rounded-xl p-2 sm:p-3 text-center shadow-lg flex-1 sm:w-full sm:max-w-[110px]">
<div className="text-[9px] text-green-200 font-medium uppercase tracking-wider">NFT ID</div> <div className="text-[8px] sm:text-[9px] text-green-200 font-medium uppercase tracking-wider">NFT ID</div>
<div className="text-white font-bold text-sm mt-1"> <div className="text-white font-bold text-xs sm:text-sm mt-1">
{nftDetails.citizenNFT ? `#${nftDetails.citizenNFT.collectionId}-${nftDetails.citizenNFT.itemId}` : 'N/A'} {nftDetails.citizenNFT ? `#${nftDetails.citizenNFT.collectionId}-${nftDetails.citizenNFT.itemId}` : 'N/A'}
</div> </div>
</div> </div>
{/* Citizen Number Badge */} {/* Citizen Number Badge */}
<div className="bg-gradient-to-br from-red-700 to-red-900 rounded-xl p-3 text-center shadow-lg w-full max-w-[110px]"> <div className="bg-gradient-to-br from-red-700 to-red-900 rounded-xl p-2 sm:p-3 text-center shadow-lg flex-1 sm:w-full sm:max-w-[110px]">
<div className="text-[9px] text-red-200 font-medium uppercase tracking-wider">Hejmara Welatî</div> <div className="text-[8px] sm:text-[9px] text-red-200 font-medium uppercase tracking-wider">Hejmara Welatî</div>
<div className="text-[8px] text-red-300 mb-1">Citizen No</div> <div className="text-[7px] sm:text-[8px] text-red-300 mb-1">Citizen No</div>
<div className="text-white font-bold text-[11px]"> <div className="text-white font-bold text-[10px] sm:text-[11px]">
{nftDetails.citizenNFT ? `#${nftDetails.citizenNFT.collectionId}-${nftDetails.citizenNFT.itemId}-${citizenNumber}` : 'N/A'} {nftDetails.citizenNFT ? `#${nftDetails.citizenNFT.collectionId}-${nftDetails.citizenNFT.itemId}-${citizenNumber}` : 'N/A'}
</div> </div>
</div> </div>
@@ -571,7 +571,7 @@ export default function Citizens() {
{/* Verified Badge */} {/* Verified Badge */}
<div className="flex items-center gap-1 bg-white/90 rounded-full px-2 py-1 shadow"> <div className="flex items-center gap-1 bg-white/90 rounded-full px-2 py-1 shadow">
<ShieldCheck className="h-3 w-3 text-green-600" /> <ShieldCheck className="h-3 w-3 text-green-600" />
<span className="text-[9px] font-semibold text-green-700">VERIFIED</span> <span className="text-[8px] sm:text-[9px] font-semibold text-green-700">VERIFIED</span>
</div> </div>
</div> </div>
</div> </div>
+6 -6
View File
@@ -530,12 +530,12 @@ export default function Dashboard() {
</div> </div>
<Tabs defaultValue="profile" className="space-y-4"> <Tabs defaultValue="profile" className="space-y-4">
<TabsList> <TabsList className="flex flex-wrap gap-1">
<TabsTrigger value="profile">Profile</TabsTrigger> <TabsTrigger value="profile" className="text-xs sm:text-sm px-2 sm:px-3">Profile</TabsTrigger>
<TabsTrigger value="roles">Roles & Tikis</TabsTrigger> <TabsTrigger value="roles" className="text-xs sm:text-sm px-2 sm:px-3">Roles & Tikis</TabsTrigger>
<TabsTrigger value="referrals">Referrals</TabsTrigger> <TabsTrigger value="referrals" className="text-xs sm:text-sm px-2 sm:px-3">Referrals</TabsTrigger>
<TabsTrigger value="security">Security</TabsTrigger> <TabsTrigger value="security" className="text-xs sm:text-sm px-2 sm:px-3">Security</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger> <TabsTrigger value="activity" className="text-xs sm:text-sm px-2 sm:px-3">Activity</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="profile" className="space-y-4"> <TabsContent value="profile" className="space-y-4">
+8
View File
@@ -19,6 +19,14 @@ export default {
'2xl': '1400px' '2xl': '1400px'
} }
}, },
screens: {
'xs': '480px',
'sm': '640px',
'md': '768px',
'lg': '1024px',
'xl': '1280px',
'2xl': '1536px',
},
extend: { extend: {
colors: { colors: {
border: 'hsl(var(--border))', border: 'hsl(var(--border))',
+8 -5
View File
@@ -30,7 +30,14 @@ export default defineConfig(() => ({
}, },
plugins: [ plugins: [
react(), react(),
nodePolyfills(), nodePolyfills({
globals: {
Buffer: true,
global: true,
process: true,
},
protocolImports: true,
}),
].filter(Boolean), ].filter(Boolean),
resolve: { resolve: {
mainFields: ['module', 'main', 'exports'], mainFields: ['module', 'main', 'exports'],
@@ -43,10 +50,6 @@ export default defineConfig(() => ({
"@local/types": path.resolve(__dirname, "../shared/types"), "@local/types": path.resolve(__dirname, "../shared/types"),
"@pezkuwi/components": path.resolve(__dirname, "../shared/components"), "@pezkuwi/components": path.resolve(__dirname, "../shared/components"),
"@shared": path.resolve(__dirname, "../shared"), "@shared": path.resolve(__dirname, "../shared"),
// Node polyfill shims for shared folder (outside web workspace)
'vite-plugin-node-polyfills/shims/buffer': path.resolve(__dirname, './src/tests/mocks/buffer-shim.ts'),
'vite-plugin-node-polyfills/shims/global': path.resolve(__dirname, './src/tests/mocks/global-shim.ts'),
'vite-plugin-node-polyfills/shims/process': path.resolve(__dirname, './src/tests/mocks/process-shim.ts'),
}, },
dedupe: ['react', 'lucide-react', 'sonner', '@pezkuwi/util-crypto', '@pezkuwi/util', '@pezkuwi/api', '@pezkuwi/extension-dapp', '@pezkuwi/keyring'], dedupe: ['react', 'lucide-react', 'sonner', '@pezkuwi/util-crypto', '@pezkuwi/util', '@pezkuwi/api', '@pezkuwi/extension-dapp', '@pezkuwi/keyring'],
}, },