mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-03 13:23:08 +00:00
chore: UI improvements
This commit is contained in:
parent
612d5595d7
commit
4892614df8
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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'
|
||||
}`}
|
||||
|
||||
@ -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 {
|
||||
|
||||
137
src/index.css
137
src/index.css
@ -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 {
|
||||
|
||||
@ -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',
|
||||
};
|
||||
};
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 !==
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user