chore: UI improvements

This commit is contained in:
Danish Arora 2025-09-05 20:26:29 +05:30
parent 612d5595d7
commit 4892614df8
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
15 changed files with 927 additions and 625 deletions

View File

@ -8,6 +8,9 @@ import {
Loader2,
TrendingUp,
Clock,
Grid3X3,
Shield,
Hash,
} from 'lucide-react';
import { CreateCellDialog } from './CreateCellDialog';
import { Button } from '@/components/ui/button';
@ -25,6 +28,49 @@ import { sortCells, SortOption } from '@/lib/utils/sorting';
import { Cell } from '@/types/forum';
import { usePending } from '@/hooks/usePending';
// Empty State Component
const EmptyState: React.FC<{ canCreateCell: boolean }> = ({ canCreateCell }) => {
return (
<div className="col-span-2 flex flex-col items-center justify-center py-16 px-4">
{/* Visual Element */}
<div className="relative mb-8">
<div className="w-32 h-32 bg-cyber-muted/20 border border-cyber-muted/30 rounded-lg flex items-center justify-center">
<Grid3X3 className="w-16 h-16 text-cyber-accent/50" />
</div>
{/* Floating elements */}
<div className="absolute -top-2 -right-2 w-6 h-6 bg-cyber-accent/20 border border-cyber-accent/30 rounded-full flex items-center justify-center">
<Hash className="w-3 h-3 text-cyber-accent" />
</div>
<div className="absolute -bottom-2 -left-2 w-6 h-6 bg-green-500/20 border border-green-500/30 rounded-full flex items-center justify-center">
<Shield className="w-3 h-3 text-green-400" />
</div>
</div>
{/* Content */}
<div className="text-center max-w-2xl">
<h2 className="text-2xl font-mono font-bold text-white mb-4">
No Cells Found
</h2>
{canCreateCell ? (
<div className="space-y-4">
<p className="text-cyber-neutral">
Ready to start your own decentralized community?
</p>
<CreateCellDialog />
</div>
) : (
<div className="p-4 bg-orange-500/10 border border-orange-500/30 rounded-md">
<p className="text-orange-400 text-sm">
Connect your wallet and verify ownership to create cells
</p>
</div>
)}
</div>
</div>
);
};
// Separate component to properly use hooks
const CellItem: React.FC<{ cell: Cell }> = ({ cell }) => {
const pending = usePending(cell.id);
@ -32,7 +78,7 @@ const CellItem: React.FC<{ cell: Cell }> = ({ cell }) => {
return (
<Link
to={`/cell/${cell.id}`}
className="group block p-4 border border-cyber-muted rounded-sm bg-cyber-muted/10 hover:bg-cyber-muted/20 hover:border-cyber-accent/50 transition-all duration-200"
className="group block board-card"
>
<div className="flex items-start gap-4">
<CypherImage
@ -109,84 +155,81 @@ const CellList = () => {
);
}
const hasCells = sortedCells.length > 0;
return (
<div className="container mx-auto px-4 py-8 max-w-6xl">
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-3xl font-bold text-glow mb-2">
Decentralized Cells
</h1>
<p className="text-cyber-neutral">
Discover communities built on Bitcoin Ordinals
</p>
</div>
<div className="page-main">
<div className="page-header">
<div className="flex justify-between items-center">
<div>
<h1 className="page-title">
Decentralized Cells
</h1>
<p className="page-subtitle">
Discover communities built on Bitcoin Ordinals
</p>
</div>
<div className="flex items-center gap-4">
<ModerationToggle />
{/* Only show controls when cells exist */}
{hasCells && (
<div className="flex items-center gap-4">
<ModerationToggle />
<Select
value={sortOption}
onValueChange={(value: SortOption) => setSortOption(value)}
>
<SelectTrigger className="w-40 bg-cyber-muted/50 border-cyber-muted">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="relevance">
<TrendingUp className="w-4 h-4 mr-2 inline" />
Relevance
</SelectItem>
<SelectItem value="activity">
<MessageSquare className="w-4 h-4 mr-2 inline" />
Activity
</SelectItem>
<SelectItem value="newest">
<Clock className="w-4 h-4 mr-2 inline" />
Newest
</SelectItem>
<SelectItem value="alphabetical">
<Layout className="w-4 h-4 mr-2 inline" />
A-Z
</SelectItem>
</SelectContent>
</Select>
<Select
value={sortOption}
onValueChange={(value: SortOption) => setSortOption(value)}
>
<SelectTrigger className="w-40 bg-cyber-muted/50 border-cyber-muted text-cyber-light">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-cyber-dark border-cyber-muted/30">
<SelectItem value="relevance" className="text-cyber-light hover:bg-cyber-muted/30">
<TrendingUp className="w-4 h-4 mr-2 inline" />
Relevance
</SelectItem>
<SelectItem value="activity" className="text-cyber-light hover:bg-cyber-muted/30">
<MessageSquare className="w-4 h-4 mr-2 inline" />
Activity
</SelectItem>
<SelectItem value="newest" className="text-cyber-light hover:bg-cyber-muted/30">
<Clock className="w-4 h-4 mr-2 inline" />
Newest
</SelectItem>
<SelectItem value="alphabetical" className="text-cyber-light hover:bg-cyber-muted/30">
<Layout className="w-4 h-4 mr-2 inline" />
A-Z
</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
size="icon"
onClick={refreshData}
disabled={isInitialLoading}
title="Refresh data"
className="px-3"
>
<RefreshCw
className={`w-4 h-4 ${isInitialLoading ? 'animate-spin' : ''}`}
/>
</Button>
<CreateCellDialog />
<Button
variant="outline"
size="icon"
onClick={refreshData}
disabled={isInitialLoading}
title="Refresh data"
className="px-3 border-cyber-muted/30 text-cyber-neutral hover:bg-cyber-muted/30"
>
<RefreshCw
className={`w-4 h-4 ${isInitialLoading ? 'animate-spin' : ''}`}
/>
</Button>
{canCreateCell && (
<CreateCellDialog />
)}
</div>
)}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{sortedCells.length === 0 ? (
<div className="col-span-2 text-center py-12">
<div className="text-cyber-neutral mb-4">
No cells found. Be the first to create one!
</div>
</div>
{!hasCells ? (
<EmptyState canCreateCell={canCreateCell} />
) : (
sortedCells.map(cell => <CellItem key={cell.id} cell={cell} />)
)}
</div>
{canCreateCell && (
<div className="text-center mt-8">
<p className="text-cyber-neutral text-sm mb-4">
Ready to start your own community?
</p>
<CreateCellDialog />
</div>
)}
</div>
);
};

View File

@ -7,6 +7,7 @@ import { useForum } from '@/contexts/useForum';
import { localDatabase } from '@/lib/database/LocalDatabase';
import { DelegationFullStatus } from '@/lib/delegation';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
LogOut,
@ -19,12 +20,18 @@ import {
Grid3X3,
User,
Bookmark,
Settings,
Menu,
X,
Clock,
} from 'lucide-react';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useToast } from '@/components/ui/use-toast';
import { useAppKitAccount, useDisconnect } from '@reown/appkit/react';
import { WalletWizard } from '@/components/ui/wallet-wizard';
@ -58,6 +65,7 @@ const Header = () => {
: undefined;
const [walletWizardOpen, setWalletWizardOpen] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
// ✅ Get display name from enhanced hook
const { displayName } = useUserDisplay(address || '');
@ -101,6 +109,10 @@ const Header = () => {
setWalletWizardOpen(true);
};
const handleOpenWizard = () => {
setWalletWizardOpen(true);
};
const handleDisconnect = async () => {
await disconnect();
await setHasShownWizard(false); // Reset so wizard can show again on next connection
@ -110,36 +122,6 @@ const Header = () => {
});
};
const getAccountStatusText = () => {
if (!isConnected) return 'Connect Wallet';
if (verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED) {
return delegationInfo?.isValid ? 'Ready to Post' : 'Delegation Expired';
} else if (verificationStatus === EVerificationStatus.WALLET_CONNECTED) {
return 'Verified (Read-only)';
} else {
return 'Verify Wallet';
}
};
const getStatusColor = () => {
if (!isConnected) return 'text-red-400';
if (
verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED &&
delegationInfo?.isValid
) {
return 'text-green-400';
} else if (verificationStatus === EVerificationStatus.WALLET_CONNECTED) {
return 'text-yellow-400';
} else if (
verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED
) {
return 'text-orange-400';
} else {
return 'text-red-400';
}
};
const getStatusIcon = () => {
if (!isConnected) return <CircleSlash className="w-4 h-4" />;
@ -161,140 +143,278 @@ const Header = () => {
};
return (
<header className="bg-cyber-muted/20 border-b border-cyber-muted sticky top-0 z-50 backdrop-blur-sm">
<div className="container mx-auto px-4 py-3">
<div className="flex items-center justify-between">
{/* Logo and Navigation */}
<div className="flex items-center space-x-6">
<Link
to="/"
className="text-xl font-bold text-glow hover:text-cyber-accent transition-colors"
>
<Terminal className="w-6 h-6 inline mr-2" />
opchan
</Link>
<nav className="hidden md:flex space-x-4">
<>
<header className="bg-black/80 border-b border-cyber-muted/30 sticky top-0 z-50 backdrop-blur-md">
<div className="container mx-auto px-4">
{/* Top Row - Logo, Network Status, User Actions */}
<div className="flex items-center justify-between h-16">
{/* Left: Logo */}
<div className="flex items-center">
<Link
to="/"
className={`flex items-center space-x-1 px-3 py-1 rounded-sm text-sm transition-colors ${
className="flex items-center space-x-2 text-xl font-mono font-bold text-white hover:text-cyber-accent transition-colors"
>
<Terminal className="w-6 h-6" />
<span className="tracking-wider">opchan</span>
</Link>
</div>
{/* Center: Network Status (Desktop) */}
<div className="hidden lg:flex items-center space-x-3">
<div className="flex items-center space-x-2 px-3 py-1 bg-cyber-muted/20 rounded-full border border-cyber-muted/30">
<WakuHealthDot />
<span className="text-xs font-mono text-cyber-neutral">
{wakuHealth.statusMessage}
</span>
{forum.lastSync && (
<div className="flex items-center space-x-1 text-xs text-cyber-neutral/70">
<Clock className="w-3 h-3" />
<span>
{new Date(forum.lastSync).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
</span>
</div>
)}
</div>
</div>
{/* Right: User Actions */}
<div className="flex items-center space-x-3">
{/* Network Status (Mobile) */}
<div className="lg:hidden">
<WakuHealthDot />
</div>
{/* User Status & Actions */}
{isConnected ? (
<div className="flex items-center space-x-2">
{/* Status Badge */}
<Badge
variant="outline"
className={`font-mono text-xs border-0 ${
verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED && delegationInfo?.isValid
? 'bg-green-500/20 text-green-400 border-green-500/30'
: verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED
? 'bg-orange-500/20 text-orange-400 border-orange-500/30'
: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30'
}`}
>
{getStatusIcon()}
<span className="ml-1">
{verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED && delegationInfo?.isValid
? 'READY'
: verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED
? 'EXPIRED'
: 'VERIFY'
}
</span>
</Badge>
{/* User Dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="flex items-center space-x-2 text-white hover:bg-cyber-muted/30"
>
<div className="text-sm font-mono">{displayName}</div>
<Settings className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56 bg-black/95 border-cyber-muted/30">
<div className="px-3 py-2 border-b border-cyber-muted/30">
<div className="text-sm font-medium text-white">{displayName}</div>
<div className="text-xs text-cyber-neutral">{address?.slice(0, 8)}...{address?.slice(-4)}</div>
</div>
<DropdownMenuItem asChild>
<Link to="/profile" className="flex items-center space-x-2">
<User className="w-4 h-4" />
<span>Profile</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/bookmarks" className="flex items-center space-x-2">
<Bookmark className="w-4 h-4" />
<span>Bookmarks</span>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator className="bg-cyber-muted/30" />
<DropdownMenuItem onClick={handleOpenWizard} className="flex items-center space-x-2">
<Settings className="w-4 h-4" />
<span>Setup Wizard</span>
</DropdownMenuItem>
<DropdownMenuSeparator className="bg-cyber-muted/30" />
<DropdownMenuItem
onClick={handleDisconnect}
className="flex items-center space-x-2 text-red-400 focus:text-red-400"
>
<LogOut className="w-4 h-4" />
<span>Disconnect</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
) : (
<Button
onClick={handleConnect}
className="bg-cyber-accent hover:bg-cyber-accent/80 text-black font-mono font-medium"
>
Connect
</Button>
)}
{/* Mobile Menu Toggle */}
<Button
variant="ghost"
size="sm"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="md:hidden text-white hover:bg-cyber-muted/30"
>
{mobileMenuOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</Button>
</div>
</div>
{/* Navigation Bar (Desktop) */}
<div className="hidden md:flex items-center justify-center border-t border-cyber-muted/20 py-2">
<nav className="flex items-center space-x-1">
<Link
to="/"
className={`flex items-center space-x-2 px-4 py-2 rounded-md text-sm font-mono transition-all ${
location.pathname === '/'
? 'bg-cyber-accent/20 text-cyber-accent'
: 'text-cyber-neutral hover:text-cyber-accent hover:bg-cyber-muted/50'
? 'bg-cyber-accent/20 text-cyber-accent border border-cyber-accent/30'
: 'text-cyber-neutral hover:text-white hover:bg-cyber-muted/20'
}`}
>
<Home className="w-4 h-4" />
<span>Home</span>
<span>HOME</span>
</Link>
<Link
to="/cells"
className={`flex items-center space-x-1 px-3 py-1 rounded-sm text-sm transition-colors ${
className={`flex items-center space-x-2 px-4 py-2 rounded-md text-sm font-mono transition-all ${
location.pathname === '/cells'
? 'bg-cyber-accent/20 text-cyber-accent'
: 'text-cyber-neutral hover:text-cyber-accent hover:bg-cyber-muted/50'
? 'bg-cyber-accent/20 text-cyber-accent border border-cyber-accent/30'
: 'text-cyber-neutral hover:text-white hover:bg-cyber-muted/20'
}`}
>
<Grid3X3 className="w-4 h-4" />
<span>Cells</span>
<span>CELLS</span>
</Link>
{isConnected && (
<>
<Link
to="/bookmarks"
className={`flex items-center space-x-1 px-3 py-1 rounded-sm text-sm transition-colors ${
className={`flex items-center space-x-2 px-4 py-2 rounded-md text-sm font-mono transition-all ${
location.pathname === '/bookmarks'
? 'bg-cyber-accent/20 text-cyber-accent'
: 'text-cyber-neutral hover:text-cyber-accent hover:bg-cyber-muted/50'
? 'bg-cyber-accent/20 text-cyber-accent border border-cyber-accent/30'
: 'text-cyber-neutral hover:text-white hover:bg-cyber-muted/20'
}`}
>
<Bookmark className="w-4 h-4" />
<span>Bookmarks</span>
<span>BOOKMARKS</span>
</Link>
<Link
to="/profile"
className={`flex items-center space-x-1 px-3 py-1 rounded-sm text-sm transition-colors ${
className={`flex items-center space-x-2 px-4 py-2 rounded-md text-sm font-mono transition-all ${
location.pathname === '/profile'
? 'bg-cyber-accent/20 text-cyber-accent'
: 'text-cyber-neutral hover:text-cyber-accent hover:bg-cyber-muted/50'
? 'bg-cyber-accent/20 text-cyber-accent border border-cyber-accent/30'
: 'text-cyber-neutral hover:text-white hover:bg-cyber-muted/20'
}`}
>
<User className="w-4 h-4" />
<span>Profile</span>
<span>PROFILE</span>
</Link>
</>
)}
</nav>
</div>
{/* Right side - Status and User */}
<div className="flex items-center space-x-4">
{/* Network Status */}
<div className="flex items-center space-x-2">
<WakuHealthDot />
<span className="text-xs text-cyber-neutral">
{wakuHealth.statusMessage}
</span>
{forum.lastSync && (
<span className="text-xs text-cyber-neutral ml-2">
Last updated{' '}
{new Date(forum.lastSync).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})}
{forum.isSyncing ? ' • syncing…' : ''}
</span>
)}
</div>
{/* User Status */}
{isConnected ? (
<div className="flex items-center space-x-3">
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center space-x-2">
<div className={getStatusColor()}>{getStatusIcon()}</div>
<div className="text-sm">
<div className="font-medium">{displayName}</div>
<div className={`text-xs ${getStatusColor()}`}>
{getAccountStatusText()}
</div>
</div>
</div>
</TooltipTrigger>
<TooltipContent>
<div className="text-xs">
<div>Address: {address?.slice(0, 8)}...</div>
<div>Status: {getAccountStatusText()}</div>
{delegationInfo?.timeRemaining && (
<div>
Delegation: {delegationInfo.timeRemaining} remaining
</div>
)}
</div>
</TooltipContent>
</Tooltip>
<Button
variant="ghost"
size="sm"
onClick={handleDisconnect}
className="text-cyber-neutral hover:text-red-400"
{/* Mobile Navigation */}
{mobileMenuOpen && (
<div className="md:hidden border-t border-cyber-muted/20 py-4 space-y-2">
<nav className="space-y-1">
<Link
to="/"
className={`flex items-center space-x-3 px-4 py-3 rounded-md text-sm font-mono transition-all ${
location.pathname === '/'
? 'bg-cyber-accent/20 text-cyber-accent border border-cyber-accent/30'
: 'text-cyber-neutral hover:text-white hover:bg-cyber-muted/20'
}`}
onClick={() => setMobileMenuOpen(false)}
>
<LogOut className="w-4 h-4" />
</Button>
<Home className="w-4 h-4" />
<span>HOME</span>
</Link>
<Link
to="/cells"
className={`flex items-center space-x-3 px-4 py-3 rounded-md text-sm font-mono transition-all ${
location.pathname === '/cells'
? 'bg-cyber-accent/20 text-cyber-accent border border-cyber-accent/30'
: 'text-cyber-neutral hover:text-white hover:bg-cyber-muted/20'
}`}
onClick={() => setMobileMenuOpen(false)}
>
<Grid3X3 className="w-4 h-4" />
<span>CELLS</span>
</Link>
{isConnected && (
<>
<Link
to="/bookmarks"
className={`flex items-center space-x-3 px-4 py-3 rounded-md text-sm font-mono transition-all ${
location.pathname === '/bookmarks'
? 'bg-cyber-accent/20 text-cyber-accent border border-cyber-accent/30'
: 'text-cyber-neutral hover:text-white hover:bg-cyber-muted/20'
}`}
onClick={() => setMobileMenuOpen(false)}
>
<Bookmark className="w-4 h-4" />
<span>BOOKMARKS</span>
</Link>
<Link
to="/profile"
className={`flex items-center space-x-3 px-4 py-3 rounded-md text-sm font-mono transition-all ${
location.pathname === '/profile'
? 'bg-cyber-accent/20 text-cyber-accent border border-cyber-accent/30'
: 'text-cyber-neutral hover:text-white hover:bg-cyber-muted/20'
}`}
onClick={() => setMobileMenuOpen(false)}
>
<User className="w-4 h-4" />
<span>PROFILE</span>
</Link>
</>
)}
</nav>
{/* Mobile Network Status */}
<div className="px-4 py-3 border-t border-cyber-muted/20">
<div className="flex items-center space-x-2 text-xs text-cyber-neutral">
<WakuHealthDot />
<span>{wakuHealth.statusMessage}</span>
{forum.lastSync && (
<span className="ml-auto">
{new Date(forum.lastSync).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
</span>
)}
</div>
</div>
) : (
<Button
onClick={handleConnect}
className="bg-cyber-accent hover:bg-cyber-accent/80"
>
Connect Wallet
</Button>
)}
</div>
</div>
)}
</div>
</div>
</header>
{/* Wallet Wizard */}
<WalletWizard
@ -308,7 +428,7 @@ const Header = () => {
});
}}
/>
</header>
</>
);
};

View File

@ -99,7 +99,7 @@ const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => {
};
return (
<div className="bg-cyber-muted/20 border border-cyber-muted rounded-sm hover:border-cyber-accent/50 hover:bg-cyber-muted/30 transition-all duration-200 mb-2">
<div className="thread-card mb-2">
<div className="flex">
{/* Voting column */}
<div className="flex flex-col items-center p-2 bg-cyber-muted/50 border-r border-cyber-muted">

View File

@ -48,7 +48,7 @@ const PostDetail = () => {
isBookmarked,
loading: bookmarkLoading,
toggleBookmark,
} = usePostBookmark(post!, post?.cellId);
} = usePostBookmark(post, post?.cellId);
// ✅ Move ALL hook calls to the top, before any conditional logic
const postPending = usePending(post?.id);

View File

@ -151,8 +151,8 @@ const PostList = () => {
};
return (
<div className="container mx-auto px-4 py-8 max-w-4xl">
<div className="mb-6">
<div className="page-main">
<div className="content-spacing">
<Link
to="/"
className="text-cyber-accent hover:underline flex items-center gap-1 text-sm"
@ -161,7 +161,7 @@ const PostList = () => {
</Link>
</div>
<div className="flex gap-4 items-start mb-6">
<div className="flex gap-4 items-start content-spacing">
<CypherImage
src={cell.icon}
alt={cell.name}
@ -170,7 +170,7 @@ const PostList = () => {
/>
<div className="flex-1">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-glow">{cell.name}</h1>
<h1 className="page-title text-glow">{cell.name}</h1>
<Button
variant="outline"
size="icon"
@ -183,12 +183,12 @@ const PostList = () => {
/>
</Button>
</div>
<p className="text-cyber-neutral">{cell.description}</p>
<p className="page-subtitle">{cell.description}</p>
</div>
</div>
{canPost && (
<div className="mb-8">
<div className="section-spacing">
<form onSubmit={handleCreatePost}>
<h2 className="text-sm font-bold mb-2 flex items-center gap-1">
<MessageSquare className="w-4 h-4" />
@ -228,7 +228,7 @@ const PostList = () => {
{!canPost &&
verificationStatus === EVerificationStatus.WALLET_CONNECTED && (
<div className="mb-8 p-4 border border-cyber-muted rounded-sm bg-cyber-muted/20">
<div className="section-spacing content-card-sm">
<div className="flex items-center gap-2 mb-2">
<Eye className="w-4 h-4 text-cyber-neutral" />
<h3 className="font-medium">Read-Only Mode</h3>
@ -244,7 +244,7 @@ const PostList = () => {
)}
{!canPost && !currentUser && (
<div className="mb-8 p-4 border border-cyber-muted rounded-sm bg-cyber-muted/20 text-center">
<div className="section-spacing content-card-sm text-center">
<p className="text-sm mb-3">
Connect wallet and verify Ordinal ownership to post
</p>
@ -256,10 +256,10 @@ const PostList = () => {
<div className="space-y-4">
{visiblePosts.length === 0 ? (
<div className="text-center py-12">
<MessageCircle className="w-12 h-12 mx-auto mb-4 text-cyber-neutral opacity-50" />
<h2 className="text-xl font-bold mb-2">No Threads Yet</h2>
<p className="text-cyber-neutral">
<div className="empty-state">
<MessageCircle className="empty-state-icon text-cyber-neutral opacity-50" />
<h2 className="empty-state-title">No Threads Yet</h2>
<p className="empty-state-description">
{canPost
? 'Be the first to post in this cell!'
: 'Connect your wallet and verify Ordinal ownership to start a thread.'}
@ -269,7 +269,7 @@ const PostList = () => {
visiblePosts.map(post => (
<div
key={post.id}
className="post-card p-4 border border-cyber-muted rounded-sm bg-cyber-muted/20 hover:bg-cyber-muted/30 transition duration-200"
className="thread-card"
>
<div className="flex gap-4">
<div className="flex flex-col items-center">

View File

@ -12,6 +12,7 @@ import { Bookmark, BookmarkType } from '@/types/forum';
import { useUserDisplay } from '@/hooks';
import { cn } from '@/lib/utils';
import { formatDistanceToNow } from 'date-fns';
import { useNavigate } from 'react-router-dom';
interface BookmarkCardProps {
bookmark: Bookmark;
@ -27,6 +28,7 @@ export function BookmarkCard({
className,
}: BookmarkCardProps) {
const authorInfo = useUserDisplay(bookmark.author || '');
const navigate = useNavigate();
const handleNavigate = () => {
if (onNavigate) {
@ -34,9 +36,9 @@ export function BookmarkCard({
} else {
// Default navigation behavior
if (bookmark.type === BookmarkType.POST) {
window.location.href = `/post/${bookmark.targetId}`;
navigate(`/post/${bookmark.targetId}`);
} else if (bookmark.type === BookmarkType.COMMENT && bookmark.postId) {
window.location.href = `/post/${bookmark.postId}#comment-${bookmark.targetId}`;
navigate(`/post/${bookmark.postId}#comment-${bookmark.targetId}`);
}
}
};

View File

@ -9,9 +9,8 @@ import {
import { Button } from '@/components/ui/button';
import { CheckCircle, Circle, Loader2 } from 'lucide-react';
import { useAuth } from '@/hooks';
import { useAuth as useAuthContext } from '@/contexts/useAuth';
import { useDelegation } from '@/hooks/useDelegation';
import { EVerificationStatus } from '@/types/identity';
import { DelegationFullStatus } from '@/lib/delegation';
import { WalletConnectionStep } from './wallet-connection-step';
import { VerificationStep } from './verification-step';
import { DelegationStep } from './delegation-step';
@ -32,43 +31,15 @@ export function WalletWizard({
const [currentStep, setCurrentStep] = React.useState<WizardStep>(1);
const [isLoading, setIsLoading] = React.useState(false);
const { isAuthenticated, verificationStatus } = useAuth();
const { getDelegationStatus } = useAuthContext();
const [delegationInfo, setDelegationInfo] =
React.useState<DelegationFullStatus | null>(null);
const hasInitialized = React.useRef(false);
const { delegationStatus } = useDelegation();
// Load delegation status
// Reset wizard when opened - always start at step 1 for simplicity
React.useEffect(() => {
getDelegationStatus().then(setDelegationInfo).catch(console.error);
}, [getDelegationStatus]);
// Reset wizard when opened and determine starting step
React.useEffect(() => {
if (open && !hasInitialized.current) {
// Determine the appropriate starting step based on current state
if (!isAuthenticated) {
setCurrentStep(1); // Start at connection step if not authenticated
} else if (
isAuthenticated &&
verificationStatus === EVerificationStatus.WALLET_UNCONNECTED
) {
setCurrentStep(2); // Start at verification step if authenticated but not verified
} else if (
isAuthenticated &&
(verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED ||
verificationStatus === EVerificationStatus.WALLET_CONNECTED) &&
!delegationInfo?.isValid
) {
setCurrentStep(3); // Start at delegation step if verified but no valid delegation
} else {
setCurrentStep(3); // Default to step 3 if everything is complete
}
if (open) {
setCurrentStep(1);
setIsLoading(false);
hasInitialized.current = true;
} else if (!open) {
hasInitialized.current = false;
}
}, [open, isAuthenticated, verificationStatus, delegationInfo]);
}, [open]);
const handleStepComplete = (step: WizardStep) => {
if (step < 3) {
@ -84,30 +55,36 @@ export function WalletWizard({
onOpenChange(false);
};
// Business logic: determine step status based on current wizard step
const getStepStatus = (step: WizardStep) => {
if (step === 1) {
return isAuthenticated ? 'complete' : 'current';
} else if (step === 2) {
if (!isAuthenticated) return 'disabled';
return verificationStatus !== EVerificationStatus.WALLET_UNCONNECTED
? 'complete'
: 'current';
} else if (step === 3) {
if (
!isAuthenticated ||
verificationStatus === EVerificationStatus.WALLET_UNCONNECTED
) {
return 'disabled';
}
return delegationInfo?.isValid ? 'complete' : 'current';
if (step < currentStep) {
return 'complete';
} else if (step === currentStep) {
return 'current';
} else {
return 'disabled';
}
return 'disabled';
};
const renderStepIcon = (step: WizardStep) => {
const status = getStepStatus(step);
// Check if step is actually completed based on auth state
const isActuallyComplete = (step: WizardStep): boolean => {
switch (step) {
case 1:
return isAuthenticated;
case 2:
return verificationStatus !== EVerificationStatus.WALLET_UNCONNECTED;
case 3:
return delegationStatus.isValid;
default:
return false;
}
};
if (status === 'complete') {
if (status === 'complete' || isActuallyComplete(step)) {
return <CheckCircle className="h-5 w-5 text-green-500" />;
} else if (status === 'current') {
return <Loader2 className="h-5 w-5 text-blue-500 animate-spin" />;
@ -149,7 +126,10 @@ export function WalletWizard({
className={`text-sm ${
getStepStatus(step as WizardStep) === 'current'
? 'text-blue-500 font-medium'
: getStepStatus(step as WizardStep) === 'complete'
: (getStepStatus(step as WizardStep) === 'complete' ||
(step === 1 && isAuthenticated) ||
(step === 2 && verificationStatus !== EVerificationStatus.WALLET_UNCONNECTED) ||
(step === 3 && delegationStatus.isValid))
? 'text-green-500'
: 'text-gray-400'
}`}
@ -160,7 +140,9 @@ export function WalletWizard({
{step < 3 && (
<div
className={`w-8 h-px mx-2 ${
getStepStatus(step as WizardStep) === 'complete'
getStepStatus(step as WizardStep) === 'complete' ||
(step === 1 && isAuthenticated) ||
(step === 2 && verificationStatus !== EVerificationStatus.WALLET_UNCONNECTED)
? 'bg-green-500'
: 'bg-gray-600'
}`}

View File

@ -161,14 +161,14 @@ export function useBookmarks() {
* Hook for bookmarking a specific post
* Provides bookmark state and toggle function for a single post
*/
export function usePostBookmark(post: Post, cellId?: string) {
export function usePostBookmark(post: Post | null, cellId?: string) {
const { currentUser } = useAuth();
const [isBookmarked, setIsBookmarked] = useState(false);
const [loading, setLoading] = useState(false);
// Check initial bookmark status
useEffect(() => {
if (currentUser?.address) {
if (currentUser?.address && post?.id) {
const bookmarked = BookmarkService.isPostBookmarked(
currentUser.address,
post.id
@ -177,10 +177,10 @@ export function usePostBookmark(post: Post, cellId?: string) {
} else {
setIsBookmarked(false);
}
}, [currentUser?.address, post.id]);
}, [currentUser?.address, post?.id]);
const toggleBookmark = useCallback(async () => {
if (!currentUser?.address) return false;
if (!currentUser?.address || !post) return false;
setLoading(true);
try {

View File

@ -113,17 +113,150 @@
}
@layer components {
/* Page Layout Components */
.page-container {
@apply min-h-screen flex flex-col bg-cyber-dark text-white;
}
.page-header {
@apply mb-8;
}
.page-title {
@apply text-3xl font-mono font-bold text-white mb-2;
}
.page-subtitle {
@apply text-cyber-neutral;
}
.page-content {
@apply flex-1 pt-16;
}
.page-main {
@apply container mx-auto px-4 py-8 max-w-6xl;
}
.page-footer {
@apply border-t border-cyber-muted py-4 text-center text-xs text-cyber-neutral;
}
/* Card Components */
.card-base {
@apply bg-cyber-muted/20 border border-cyber-muted/30 rounded-sm transition-all duration-200;
}
.card-hover {
@apply hover:bg-cyber-muted/30 hover:border-cyber-accent/50;
}
.card-padding {
@apply p-6;
}
.card-padding-sm {
@apply p-4;
}
.thread-card {
@apply border border-cyber-muted bg-cyber-dark hover:bg-cyber-muted/20 transition-colors rounded-sm p-4 mb-4;
@apply card-base card-hover card-padding-sm mb-4;
}
.board-card {
@apply border border-cyber-muted bg-cyber-dark hover:bg-cyber-muted/20 transition-colors rounded-sm p-3 mb-3;
@apply card-base card-hover card-padding-sm mb-3;
}
.comment-card {
@apply border-l-2 border-muted pl-3 py-2 my-2 hover:border-primary transition-colors;
}
/* Content Cards */
.content-card {
@apply card-base card-hover card-padding;
}
.content-card-sm {
@apply card-base card-hover card-padding-sm;
}
/* Status Indicators */
.status-ready {
@apply bg-green-500/20 text-green-400 border-green-500/30;
}
.status-warning {
@apply bg-orange-500/20 text-orange-400 border-orange-500/30;
}
.status-error {
@apply bg-red-500/20 text-red-400 border-red-500/30;
}
.status-neutral {
@apply bg-cyber-muted/20 text-cyber-neutral border-cyber-muted/30;
}
/* Button Variants */
.btn-cyber {
@apply bg-cyber-accent hover:bg-cyber-accent/80 text-black font-mono font-medium;
}
.btn-cyber-outline {
@apply border-cyber-muted/30 text-cyber-neutral hover:bg-cyber-muted/30;
}
/* Typography */
.text-glow {
text-shadow: 0 0 8px rgba(15, 160, 206, 0.5);
}
.text-glow-subtle {
text-shadow: 0 0 4px rgba(15, 160, 206, 0.3);
}
/* Spacing Utilities */
.section-spacing {
@apply mb-8;
}
.content-spacing {
@apply mb-6;
}
.item-spacing {
@apply mb-4;
}
/* Grid Layouts */
.grid-main-sidebar {
@apply grid grid-cols-1 lg:grid-cols-3 gap-6;
}
.grid-main-content {
@apply lg:col-span-2;
}
.grid-sidebar {
@apply lg:col-span-1;
}
/* Empty States */
.empty-state {
@apply flex flex-col items-center justify-center py-16 px-4 text-center;
}
.empty-state-icon {
@apply w-16 h-16 text-cyber-accent/50 mb-4;
}
.empty-state-title {
@apply text-xl font-mono font-bold text-white mb-2;
}
.empty-state-description {
@apply text-cyber-neutral mb-4;
}
}
@keyframes cyber-flicker {

View File

@ -2,7 +2,7 @@
* Single content topic for all message types
* Different message types are parsed from the message content itself
*/
export const CONTENT_TOPIC = '/opchan-sds-ab/1/messages/proto';
export const CONTENT_TOPIC = '/opchan-test-1/1/messages/proto';
/**
* Bootstrap nodes for the Waku network
@ -14,8 +14,4 @@ export const BOOTSTRAP_NODES = {
'/dns4/node-01.do-ams3.waku.sandbox.status.im/tcp/30303/p2p/16Uiu2HAmNaeL4p3WEYzC9mgXBmBWSgWjPHRvatZTXnp8Jgv3iKsb',
'/dns4/vps-aaa00d52.vps.ovh.ca/tcp/8000/wss/p2p/16Uiu2HAm9PftGgHZwWE3wzdMde4m3kT2eYJFXLZfGoSED3gysofk',
],
};
export const LOCAL_STORAGE_KEYS = {
KEY_DELEGATION: 'opchan-key-delegation',
};
};

View File

@ -1,4 +1,5 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import Header from '@/components/Header';
import { BookmarkList } from '@/components/ui/bookmark-card';
import { Button } from '@/components/ui/button';
@ -27,6 +28,7 @@ import { useAuth } from '@/contexts/useAuth';
const BookmarksPage = () => {
const { currentUser } = useAuth();
const navigate = useNavigate();
const {
bookmarks,
loading,
@ -75,9 +77,9 @@ const BookmarksPage = () => {
const handleNavigate = (bookmark: Bookmark) => {
if (bookmark.type === BookmarkType.POST) {
window.location.href = `/post/${bookmark.targetId}`;
navigate(`/post/${bookmark.targetId}`);
} else if (bookmark.type === BookmarkType.COMMENT && bookmark.postId) {
window.location.href = `/post/${bookmark.postId}#comment-${bookmark.targetId}`;
navigate(`/post/${bookmark.postId}#comment-${bookmark.targetId}`);
}
};
@ -117,19 +119,20 @@ const BookmarksPage = () => {
}
return (
<div className="min-h-screen flex flex-col bg-cyber-dark text-white">
<div className="page-container">
<Header />
<main className="flex-1 container mx-auto px-4 py-8 max-w-4xl">
{/* Header Section */}
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<BookmarkIcon className="text-cyber-accent" size={32} />
<h1 className="text-3xl font-bold text-cyber-light">
My Bookmarks
</h1>
</div>
<main className="page-content">
<div className="page-main">
{/* Header Section */}
<div className="page-header">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<BookmarkIcon className="text-cyber-accent" size={32} />
<h1 className="page-title">
My Bookmarks
</h1>
</div>
{bookmarks.length > 0 && (
<AlertDialog>
@ -165,11 +168,11 @@ const BookmarksPage = () => {
)}
</div>
<p className="text-cyber-neutral">
Your saved posts and comments. Bookmarks are stored locally and
won't be shared.
</p>
</div>
<p className="page-subtitle">
Your saved posts and comments. Bookmarks are stored locally and
won't be shared.
</p>
</div>
{/* Stats */}
{bookmarks.length > 0 && (
@ -248,9 +251,10 @@ const BookmarksPage = () => {
/>
</TabsContent>
</Tabs>
</div>
</main>
<footer className="border-t border-cyber-muted py-4 text-center text-xs text-cyber-neutral">
<footer className="page-footer">
<p>OpChan - A decentralized forum built on Waku & Bitcoin Ordinals</p>
</footer>
</div>

View File

@ -3,12 +3,12 @@ import PostList from '@/components/PostList';
const CellPage = () => {
return (
<div className="min-h-screen flex flex-col bg-cyber-dark text-white">
<div className="page-container">
<Header />
<main className="flex-1 pt-16">
<main className="page-content">
<PostList />
</main>
<footer className="border-t border-cyber-muted py-4 text-center text-xs text-cyber-neutral">
<footer className="page-footer">
<p>OpChan - A decentralized forum built on Waku & Bitcoin Ordinals</p>
</footer>
</div>

View File

@ -96,18 +96,19 @@ const FeedPage: React.FC = () => {
}
return (
<div className="min-h-screen bg-cyber-dark">
<div className="container mx-auto px-4 py-6 max-w-6xl">
<div className="page-container">
<div className="page-main">
{/* Page Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-glow text-cyber-accent">
Popular Posts
</h1>
<p className="text-cyber-neutral text-sm">
Latest posts from all cells
</p>
</div>
<div className="page-header">
<div className="flex items-center justify-between">
<div>
<h1 className="page-title text-glow text-cyber-accent">
Popular Posts
</h1>
<p className="page-subtitle">
Latest posts from all cells
</p>
</div>
<div className="flex items-center space-x-2">
<ModerationToggle />
@ -147,6 +148,7 @@ const FeedPage: React.FC = () => {
<span>Refresh</span>
</Button>
</div>
</div>
</div>
<div className="flex gap-6">
@ -155,12 +157,12 @@ const FeedPage: React.FC = () => {
{/* Posts Feed */}
<div className="space-y-0">
{allPosts.length === 0 ? (
<div className="bg-cyber-muted/20 border border-cyber-muted rounded-sm p-12 text-center">
<div className="empty-state">
<div className="space-y-3">
<h3 className="text-lg font-semibold text-glow">
<h3 className="empty-state-title text-glow">
No posts yet
</h3>
<p className="text-cyber-neutral">
<p className="empty-state-description">
Be the first to create a post in a cell!
</p>
{verificationStatus !==

View File

@ -9,9 +9,9 @@ const Index = () => {
const { refreshData } = useForumActions();
return (
<div className="min-h-screen flex flex-col bg-cyber-dark text-white">
<div className="page-container">
<Header />
<main className="flex-1 relative">
<main className="page-content relative">
<CellList />
{!health.isConnected && (
<div className="fixed bottom-4 right-4">
@ -26,7 +26,7 @@ const Index = () => {
</div>
)}
</main>
<footer className="border-t border-cyber-muted py-4 text-center text-xs text-cyber-neutral">
<footer className="page-footer">
<p>OpChan - A decentralized forum built on Waku & Bitcoin Ordinals</p>
</footer>
</div>

View File

@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { useAuth, useUserActions, useForumActions } from '@/hooks';
import { useAuth as useAuthContext } from '@/contexts/useAuth';
import { useUserDisplay } from '@/hooks';
import { useDelegation } from '@/hooks/useDelegation';
import { DelegationFullStatus } from '@/lib/delegation';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@ -15,17 +16,21 @@ import {
} from '@/components/ui/select';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { WalletWizard } from '@/components/ui/wallet-wizard';
import Header from '@/components/Header';
import {
Loader2,
Wallet,
Hash,
User,
Shield,
CheckCircle,
AlertTriangle,
XCircle,
Settings,
Copy,
Globe,
Edit3,
Save,
X,
} from 'lucide-react';
import { EDisplayPreference, EVerificationStatus } from '@/types/identity';
import { useToast } from '@/hooks/use-toast';
@ -36,8 +41,9 @@ export default function ProfilePage() {
const { toast } = useToast();
// Get current user from auth context for the address
const { currentUser } = useAuth();
const { currentUser, verificationStatus } = useAuth();
const { getDelegationStatus } = useAuthContext();
const { delegationStatus } = useDelegation();
const [delegationInfo, setDelegationInfo] =
useState<DelegationFullStatus | null>(null);
const address = currentUser?.address;
@ -68,16 +74,38 @@ export default function ProfilePage() {
}
}, [currentUser]);
// Copy to clipboard function
const copyToClipboard = async (text: string, label: string) => {
try {
await navigator.clipboard.writeText(text);
toast({
title: 'Copied!',
description: `${label} copied to clipboard`,
});
} catch {
toast({
title: 'Copy Failed',
description: 'Failed to copy to clipboard',
variant: 'destructive',
});
}
};
if (!currentUser) {
return (
<div className="container mx-auto px-4 py-8">
<Card>
<CardContent className="pt-6">
<div className="text-center text-muted-foreground">
Please connect your wallet to view your profile.
</div>
</CardContent>
</Card>
<div className="min-h-screen flex flex-col bg-cyber-dark text-white">
<Header />
<main className="flex-1 flex items-center justify-center pt-16">
<Card className="w-full max-w-md bg-cyber-muted/20 border-cyber-muted/30">
<CardContent className="pt-6">
<div className="text-center text-cyber-neutral">
<User className="w-12 h-12 mx-auto mb-4 text-cyber-accent" />
<h2 className="text-xl font-mono font-bold mb-2">Connect Required</h2>
<p className="text-sm">Please connect your wallet to view your profile.</p>
</div>
</CardContent>
</Card>
</main>
</div>
);
}
@ -146,7 +174,7 @@ export default function ProfilePage() {
};
const getVerificationIcon = () => {
switch (userInfo.verificationLevel) {
switch (verificationStatus) {
case EVerificationStatus.ENS_ORDINAL_VERIFIED:
return <CheckCircle className="h-4 w-4 text-green-500" />;
case EVerificationStatus.WALLET_CONNECTED:
@ -159,7 +187,7 @@ export default function ProfilePage() {
};
const getVerificationText = () => {
switch (userInfo.verificationLevel) {
switch (verificationStatus) {
case EVerificationStatus.ENS_ORDINAL_VERIFIED:
return 'Owns ENS or Ordinal';
case EVerificationStatus.WALLET_CONNECTED:
@ -172,7 +200,7 @@ export default function ProfilePage() {
};
const getVerificationColor = () => {
switch (userInfo.verificationLevel) {
switch (verificationStatus) {
case EVerificationStatus.ENS_ORDINAL_VERIFIED:
return 'bg-green-100 text-green-800 border-green-200';
case EVerificationStatus.WALLET_CONNECTED:
@ -185,316 +213,308 @@ export default function ProfilePage() {
};
return (
<div className="container mx-auto px-4 py-8 max-w-2xl">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="h-5 w-5" />
Profile
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Wallet Information */}
<div className="space-y-3">
<h3 className="text-lg font-semibold flex items-center gap-2">
<Wallet className="h-4 w-4" />
Wallet Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label className="text-sm font-medium text-muted-foreground">
Address
</Label>
<div className="mt-1 font-mono text-sm bg-muted px-3 py-2 rounded-md break-all">
{currentUser.address}
</div>
</div>
<div>
<Label className="text-sm font-medium text-muted-foreground">
Network
</Label>
<div className="mt-1 flex items-center gap-2">
<Badge variant="outline" className="capitalize">
{currentUser.walletType}
</Badge>
</div>
</div>
</div>
<div className="page-container">
<Header />
<main className="page-content">
<div className="page-main">
{/* Page Header */}
<div className="page-header">
<h1 className="page-title">Profile</h1>
<p className="page-subtitle">Manage your account settings and preferences</p>
</div>
<Separator />
{/* Identity Information */}
<div className="space-y-3">
<h3 className="text-lg font-semibold flex items-center gap-2">
<Hash className="h-4 w-4" />
Identity
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label className="text-sm font-medium text-muted-foreground">
ENS Name
</Label>
<div className="mt-1 text-sm">
{currentUser.ensDetails?.ensName || 'N/A'}
</div>
</div>
<div>
<Label className="text-sm font-medium text-muted-foreground">
Current Display Name
</Label>
<div className="mt-1 text-sm font-medium">
{userInfo.displayName}
</div>
</div>
</div>
</div>
<Separator />
{/* Editable Profile Fields */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Profile Settings</h3>
<div className="space-y-4">
<div>
<Label htmlFor="callSign" className="text-sm font-medium">
Call Sign
</Label>
{isEditing ? (
<Input
id="callSign"
value={callSign}
onChange={e => setCallSign(e.target.value)}
placeholder="Enter your call sign"
className="mt-1"
disabled={isSubmitting}
/>
) : (
<div className="mt-1 text-sm bg-muted px-3 py-2 rounded-md">
{userInfo.callSign || currentUser.callSign || 'Not set'}
</div>
)}
<p className="mt-1 text-xs text-muted-foreground">
3-20 characters, letters, numbers, underscores, and hyphens
only
</p>
</div>
<div>
<Label
htmlFor="displayPreference"
className="text-sm font-medium"
>
Display Preference
</Label>
{isEditing ? (
<Select
value={displayPreference}
onValueChange={value =>
setDisplayPreference(value as EDisplayPreference)
}
disabled={isSubmitting}
>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={EDisplayPreference.CALL_SIGN}>
Call Sign (when available)
</SelectItem>
<SelectItem value={EDisplayPreference.WALLET_ADDRESS}>
Wallet Address
</SelectItem>
</SelectContent>
</Select>
) : (
<div className="mt-1 text-sm bg-muted px-3 py-2 rounded-md">
{(userInfo.displayPreference || displayPreference) ===
EDisplayPreference.CALL_SIGN
? 'Call Sign (when available)'
: 'Wallet Address'}
</div>
)}
</div>
</div>
</div>
<Separator />
{/* Verification Status */}
<div className="space-y-3">
<h3 className="text-lg font-semibold flex items-center gap-2">
<Shield className="h-4 w-4" />
Verification Status
</h3>
<div className="flex items-center gap-3">
{getVerificationIcon()}
<Badge className={getVerificationColor()}>
{getVerificationText()}
</Badge>
</div>
</div>
<Separator />
{/* Delegation Details */}
<div className="space-y-3">
<h3 className="text-lg font-semibold flex items-center gap-2">
<CheckCircle className="h-4 w-4" />
Key Delegation
</h3>
<div className="space-y-4">
{/* Delegation Status */}
<div className="flex items-center gap-3">
<Badge
variant={delegationInfo?.isValid ? 'default' : 'secondary'}
className={
delegationInfo?.isValid
? 'bg-green-600 hover:bg-green-700'
: ''
}
>
{delegationInfo?.isValid ? 'Active' : 'Inactive'}
</Badge>
{delegationInfo?.isValid && delegationInfo?.timeRemaining && (
<span className="text-sm text-muted-foreground">
{delegationInfo.timeRemaining} remaining
</span>
)}
{!delegationInfo?.isValid && (
<Badge
variant="outline"
className="text-yellow-600 border-yellow-600"
>
Renewal Recommended
</Badge>
)}
{!delegationInfo?.isValid && (
<Badge variant="destructive">Expired</Badge>
)}
</div>
{/* Delegation Details Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label className="text-sm font-medium text-muted-foreground">
Browser Public Key
</Label>
<div className="mt-1 text-sm font-mono bg-muted px-3 py-2 rounded-md break-all">
{currentUser.browserPubKey
? `${currentUser.browserPubKey.slice(0, 12)}...${currentUser.browserPubKey.slice(-8)}`
: 'Not delegated'}
</div>
</div>
<div>
<Label className="text-sm font-medium text-muted-foreground">
Delegation Signature
</Label>
<div className="mt-1 text-sm">
{currentUser.delegationSignature === 'valid' ? (
<Badge
{/* Two-Card Layout: User Profile + Security Status */}
<div className="grid-main-sidebar content-spacing">
{/* User Profile Card - Primary (2/3 width) */}
<div className="grid-main-content">
<Card className="content-card">
<CardHeader>
<CardTitle className="flex items-center justify-between text-white">
<div className="flex items-center gap-2">
<User className="h-5 w-5 text-cyber-accent" />
User Profile
</div>
{!isEditing && (
<Button
variant="outline"
className="text-green-600 border-green-600"
size="sm"
onClick={() => setIsEditing(true)}
className="border-cyber-muted/30 text-cyber-neutral hover:bg-cyber-muted/30"
>
Valid
</Badge>
) : (
'Not signed'
<Edit3 className="w-4 h-4 mr-2" />
Edit
</Button>
)}
</div>
</div>
{currentUser.delegationExpiry && (
<div>
<Label className="text-sm font-medium text-muted-foreground">
Expires At
</Label>
<div className="mt-1 text-sm">
{new Date(currentUser.delegationExpiry).toLocaleString()}
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Identity Section */}
<div className="space-y-4">
<div className="flex items-center gap-4">
<div className="w-16 h-16 bg-cyber-accent/20 border border-cyber-accent/30 rounded-lg flex items-center justify-center">
<User className="w-8 h-8 text-cyber-accent" />
</div>
<div className="flex-1">
<div className="text-xl font-mono font-bold text-white">
{userInfo.displayName}
</div>
<div className="text-sm text-cyber-neutral">
{currentUser.ensDetails?.ensName || 'No ENS name'}
</div>
<div className="flex items-center gap-2 mt-2">
{getVerificationIcon()}
<Badge className={getVerificationColor()}>
{getVerificationText()}
</Badge>
</div>
</div>
</div>
</div>
)}
<div>
<Label className="text-sm font-medium text-muted-foreground">
Last Updated
</Label>
<div className="mt-1 text-sm">
{currentUser.lastChecked
? new Date(currentUser.lastChecked).toLocaleString()
: 'Never'}
{/* Wallet Section */}
<div className="space-y-3">
<h3 className="text-sm font-medium text-cyber-neutral uppercase tracking-wide">
Wallet Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-sm font-medium text-cyber-neutral">
Address
</Label>
<div className="flex items-center gap-2">
<div className="flex-1 font-mono text-sm bg-cyber-dark/50 border border-cyber-muted/30 px-3 py-2 rounded-md text-cyber-light">
{currentUser.address.slice(0, 8)}...{currentUser.address.slice(-6)}
</div>
<Button
variant="outline"
size="sm"
onClick={() => copyToClipboard(currentUser.address, 'Address')}
className="border-cyber-muted/30 text-cyber-neutral hover:bg-cyber-muted/30"
>
<Copy className="w-4 h-4" />
</Button>
</div>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium text-cyber-neutral">
Network
</Label>
<div className="flex items-center gap-2">
<Globe className="w-4 h-4 text-cyber-neutral" />
<Badge variant="outline" className="capitalize bg-cyber-accent/20 text-cyber-accent border-cyber-accent/30">
{currentUser.walletType}
</Badge>
</div>
</div>
</div>
</div>
</div>
<div>
<Label className="text-sm font-medium text-muted-foreground">
Can Delegate
</Label>
<div className="mt-1 text-sm">
{delegationInfo?.hasDelegation ? (
<Badge
{/* Settings Section */}
<div className="space-y-3">
<h3 className="text-sm font-medium text-cyber-neutral uppercase tracking-wide">
Profile Settings
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="callSign" className="text-sm font-medium text-cyber-neutral">
Call Sign
</Label>
{isEditing ? (
<Input
id="callSign"
value={callSign}
onChange={e => setCallSign(e.target.value)}
placeholder="Enter your call sign"
className="bg-cyber-dark/50 border-cyber-muted/30 text-cyber-light"
disabled={isSubmitting}
/>
) : (
<div className="text-sm bg-cyber-dark/50 border border-cyber-muted/30 px-3 py-2 rounded-md text-cyber-light">
{userInfo.callSign || currentUser.callSign || 'Not set'}
</div>
)}
<p className="text-xs text-cyber-neutral">
3-20 characters, letters, numbers, underscores, and hyphens only
</p>
</div>
<div className="space-y-2">
<Label htmlFor="displayPreference" className="text-sm font-medium text-cyber-neutral">
Display Preference
</Label>
{isEditing ? (
<Select
value={displayPreference}
onValueChange={value =>
setDisplayPreference(value as EDisplayPreference)
}
disabled={isSubmitting}
>
<SelectTrigger className="bg-cyber-dark/50 border-cyber-muted/30 text-cyber-light">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-cyber-dark border-cyber-muted/30">
<SelectItem value={EDisplayPreference.CALL_SIGN} className="text-cyber-light hover:bg-cyber-muted/30">
Call Sign (when available)
</SelectItem>
<SelectItem value={EDisplayPreference.WALLET_ADDRESS} className="text-cyber-light hover:bg-cyber-muted/30">
Wallet Address
</SelectItem>
</SelectContent>
</Select>
) : (
<div className="text-sm bg-cyber-dark/50 border border-cyber-muted/30 px-3 py-2 rounded-md text-cyber-light">
{(userInfo.displayPreference || displayPreference) ===
EDisplayPreference.CALL_SIGN
? 'Call Sign (when available)'
: 'Wallet Address'}
</div>
)}
</div>
</div>
</div>
{/* Action Buttons */}
{isEditing && (
<div className="flex justify-end gap-3 pt-4 border-t border-cyber-muted/30">
<Button
variant="outline"
className="text-green-600 border-green-600"
onClick={handleCancel}
disabled={isSubmitting}
className="border-cyber-muted/30 text-cyber-neutral hover:bg-cyber-muted/30"
>
Yes
</Badge>
) : (
<Badge
<X className="w-4 h-4 mr-2" />
Cancel
</Button>
<Button
onClick={handleSave}
disabled={isSubmitting}
className="bg-cyber-accent hover:bg-cyber-accent/80 text-black font-mono"
>
{isSubmitting ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Save className="mr-2 h-4 w-4" />
)}
Save Changes
</Button>
</div>
)}
</CardContent>
</Card>
</div>
{/* Security Status Card - Secondary (1/3 width) */}
<div className="grid-sidebar">
<Card className="content-card">
<CardHeader>
<CardTitle className="flex items-center justify-between text-white">
<div className="flex items-center gap-2">
<Shield className="h-5 w-5 text-cyber-accent" />
Security
</div>
{(delegationStatus.hasDelegation || delegationInfo?.hasDelegation) && (
<Button
variant="outline"
className="text-red-600 border-red-600"
size="sm"
onClick={() => setWalletWizardOpen(true)}
className="border-cyber-muted/30 text-cyber-neutral hover:bg-cyber-muted/30"
>
No
</Badge>
<Settings className="w-4 h-4 mr-2" />
{(delegationStatus.isValid || delegationInfo?.isValid) ? 'Renew' : 'Setup'}
</Button>
)}
</div>
</div>
</div>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Delegation Status */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-cyber-neutral">Delegation</span>
<Badge
variant={(delegationStatus.isValid || delegationInfo?.isValid) ? 'default' : 'secondary'}
className={
(delegationStatus.isValid || delegationInfo?.isValid)
? 'bg-green-500/20 text-green-400 border-green-500/30'
: 'bg-red-500/20 text-red-400 border-red-500/30'
}
>
{(delegationStatus.isValid || delegationInfo?.isValid) ? 'Active' : 'Inactive'}
</Badge>
</div>
{/* Delegation Actions */}
{delegationInfo?.hasDelegation && (
<div className="pt-2">
<Button
variant="outline"
size="sm"
onClick={() => setWalletWizardOpen(true)}
>
{delegationInfo?.isValid
? 'Renew Delegation'
: 'Delegate Key'}
</Button>
</div>
)}
{/* Expiry Date */}
{(delegationStatus.expiresAt || currentUser.delegationExpiry) && (
<div className="space-y-1">
<span className="text-xs text-cyber-neutral">Valid until</span>
<div className="text-sm font-mono text-cyber-light">
{(delegationStatus.expiresAt || new Date(currentUser.delegationExpiry!)).toLocaleDateString()}
</div>
</div>
)}
{/* Signature Status */}
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-cyber-neutral">Signature</span>
<Badge
variant="outline"
className={
(delegationStatus.isValid || currentUser.delegationSignature === 'valid')
? 'text-green-400 border-green-500/30 bg-green-500/10'
: 'text-red-400 border-red-500/30 bg-red-500/10'
}
>
{(delegationStatus.isValid || currentUser.delegationSignature === 'valid') ? 'Valid' : 'Not signed'}
</Badge>
</div>
</div>
{/* Browser Public Key */}
<div className="space-y-2">
<Label className="text-sm font-medium text-cyber-neutral">
Browser Public Key
</Label>
<div className="flex items-center gap-2">
<div className="flex-1 font-mono text-xs bg-cyber-dark/50 border border-cyber-muted/30 px-2 py-1 rounded text-cyber-light">
{(delegationStatus.publicKey || currentUser.browserPubKey)
? `${(delegationStatus.publicKey || currentUser.browserPubKey!).slice(0, 12)}...${(delegationStatus.publicKey || currentUser.browserPubKey!).slice(-8)}`
: 'Not delegated'}
</div>
{(delegationStatus.publicKey || currentUser.browserPubKey) && (
<Button
variant="outline"
size="sm"
onClick={() => copyToClipboard(delegationStatus.publicKey || currentUser.browserPubKey!, 'Public Key')}
className="border-cyber-muted/30 text-cyber-neutral hover:bg-cyber-muted/30"
>
<Copy className="w-3 h-3" />
</Button>
)}
</div>
</div>
{/* Warning for expired delegation */}
{(!delegationStatus.isValid && delegationStatus.hasDelegation) || (!delegationInfo?.isValid && delegationInfo?.hasDelegation) && (
<div className="p-3 bg-orange-500/10 border border-orange-500/30 rounded-md">
<div className="flex items-center gap-2 text-orange-400">
<AlertTriangle className="w-4 h-4" />
<span className="text-xs font-medium">
Delegation expired. Renew to continue using your browser key.
</span>
</div>
</div>
)}
</CardContent>
</Card>
</div>
</div>
<Separator />
</div>
</main>
{/* Action Buttons */}
<div className="flex justify-end gap-3 pt-4">
{isEditing ? (
<>
<Button
variant="outline"
onClick={handleCancel}
disabled={isSubmitting}
>
Cancel
</Button>
<Button onClick={handleSave} disabled={isSubmitting}>
{isSubmitting && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Save Changes
</Button>
</>
) : (
<Button onClick={() => setIsEditing(true)}>Edit Profile</Button>
)}
</div>
</CardContent>
</Card>
<footer className="page-footer">
<p>OpChan - A decentralized forum built on Waku & Bitcoin Ordinals</p>
</footer>
{/* Wallet Wizard */}
<WalletWizard