chore: raw UI

This commit is contained in:
Danish Arora 2025-11-17 18:29:28 -05:00
parent bf7b3f20a1
commit d12a76ff99
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
8 changed files with 342 additions and 1085 deletions

View File

@ -2,18 +2,9 @@ import { useState, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { useContent, usePermissions, useNetwork } from '@/hooks';
import {
Layout,
MessageSquare,
RefreshCw,
Loader2,
TrendingUp,
Clock,
Grid3X3,
Shield,
Hash,
} from 'lucide-react';
import { CreateCellDialog } from './CreateCellDialog';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
@ -21,123 +12,12 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { CypherImage } from './ui/CypherImage';
import { RelevanceIndicator } from './ui/relevance-indicator';
import { ModerationToggle } from './ui/moderation-toggle';
import { sortCells, SortOption } from '@/utils/sorting';
import type { Cell } from '@opchan/core';
import { useForum } from '@/hooks';
import { ShareButton } from './ui/ShareButton';
// 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 { content } = useForum();
const isPending = content.pending.isPending(cell.id);
return (
<Link to={`/cell/${cell.id}`} className="group block board-card">
<div className="flex items-start gap-4">
<CypherImage
src={cell.icon}
alt={cell.name}
className="w-12 h-12 object-cover rounded-sm border border-cyber-muted flex-shrink-0"
generateUniqueFallback={true}
/>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between mb-2">
<h2 className="text-lg font-bold text-glow group-hover:text-cyber-accent transition-colors line-clamp-1">
{cell.name}
</h2>
{cell.relevanceScore !== undefined && (
<RelevanceIndicator
score={cell.relevanceScore}
details={cell.relevanceDetails}
type="cell"
className="ml-2 flex-shrink-0"
showTooltip={true}
/>
)}
</div>
{isPending && (
<div className="mb-2">
<span className="px-2 py-0.5 rounded-sm bg-yellow-500/20 text-yellow-400 border border-yellow-500/30 text-xs">
syncing
</span>
</div>
)}
<p className="text-cyber-neutral text-sm mb-3 line-clamp-2">
{cell.description}
</p>
<div className="flex items-center justify-between text-xs text-cyber-neutral">
<div className="flex items-center gap-4">
<span className="flex items-center gap-1">
<MessageSquare className="w-3 h-3" />
{cell.postCount || 0} posts
</span>
<span className="flex items-center gap-1">
<Layout className="w-3 h-3" />
{cell.activeMemberCount || 0} members
</span>
</div>
<ShareButton
size="sm"
url={`${window.location.origin}/cell/${cell.id}`}
title={cell.name}
/>
</div>
</div>
</div>
</Link>
);
};
const CellList = () => {
const { cellsWithStats } = useContent();
@ -151,97 +31,76 @@ const CellList = () => {
return sortCells(cellsWithStats, sortOption);
}, [cellsWithStats, sortOption]);
// Only show loading if store is not yet hydrated
// Simple loading
if (!isHydrated && !cellsWithStats.length) {
return (
<div className="container mx-auto px-4 pt-24 pb-16 text-center">
<Loader2 className="w-8 h-8 mx-auto mb-4 animate-spin text-primary" />
<p className="text-lg font-medium text-muted-foreground">
Loading Cells...
</p>
<div className="w-full mx-auto px-2 py-4 max-w-4xl">
<div className="text-xs text-muted-foreground">Loading...</div>
</div>
);
}
const hasCells = sortedCells.length > 0;
return (
<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>
{/* 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 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={content.refresh}
disabled={false}
title="Refresh data"
className="px-3 border-cyber-muted/30 text-cyber-neutral hover:bg-cyber-muted/30"
>
<RefreshCw className="w-4 h-4" />
</Button>
{canCreateCell && <CreateCellDialog />}
</div>
)}
<div className="w-full mx-auto px-2 py-2 max-w-4xl">
<div className="mb-2 pb-1 border-b border-border/30 flex items-center justify-between text-xs">
<div className="flex items-center gap-2">
<span className="text-primary font-semibold">CELLS</span>
<ModerationToggle />
</div>
<div className="flex items-center gap-2">
<Select
value={sortOption}
onValueChange={(value: SortOption) => setSortOption(value)}
>
<SelectTrigger className="w-24 text-[10px] h-6">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="relevance">Relevance</SelectItem>
<SelectItem value="activity">Activity</SelectItem>
<SelectItem value="newest">Newest</SelectItem>
<SelectItem value="alphabetical">A-Z</SelectItem>
</SelectContent>
</Select>
<button
onClick={content.refresh}
className="text-muted-foreground hover:text-foreground text-[10px]"
>
refresh
</button>
{canCreateCell && <CreateCellDialog />}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{!hasCells ? (
<EmptyState canCreateCell={canCreateCell} />
<div>
{sortedCells.length === 0 ? (
<div className="py-4 text-xs text-muted-foreground text-center">
No cells yet. {canCreateCell ? 'Create one!' : 'Connect wallet to create.'}
</div>
) : (
sortedCells.map(cell => <CellItem key={cell.id} cell={cell} />)
sortedCells.map(cell => {
const { content } = useForum();
const isPending = content.pending.isPending(cell.id);
return (
<Link
key={cell.id}
to={`/cell/${cell.id}`}
className="border-b border-border/30 py-1.5 px-2 text-xs flex items-baseline gap-2 hover:bg-border/5"
>
<span className="text-primary font-medium">{cell.name}</span>
<span className="text-muted-foreground text-[10px] flex-1 truncate">
{cell.description}
</span>
<span className="text-muted-foreground text-[10px] whitespace-nowrap">
{cell.postCount || 0}p
</span>
{isPending && (
<span className="text-yellow-400 text-[10px]">syncing</span>
)}
</Link>
);
})
)}
</div>
</div>

View File

@ -1,55 +1,22 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Terminal, FileText, Shield, Github, BookOpen } from 'lucide-react';
const Footer: React.FC = () => {
return (
<footer className="bg-cyber-dark border-t border-border mt-auto">
<div className="max-w-6xl mx-auto px-3 sm:px-4 py-4 sm:py-6">
<div className="flex flex-col md:flex-row items-center justify-between gap-3 sm:gap-4 text-[10px] sm:text-[11px] uppercase tracking-[0.15em] sm:tracking-[0.2em] text-muted-foreground">
<div className="flex items-center space-x-2 sm:space-x-3">
<Terminal className="w-3 h-3 sm:w-4 sm:h-4 text-primary flex-shrink-0" />
<span>© 2025 OPCHAN</span>
</div>
<nav className="flex items-center flex-wrap justify-center gap-x-3 sm:gap-x-6 gap-y-2 text-[10px] sm:text-[11px] uppercase tracking-[0.15em] sm:tracking-[0.2em]">
<Link
to="/terms"
className="flex items-center space-x-1 sm:space-x-2 text-muted-foreground hover:text-foreground"
>
<FileText className="w-3 h-3 sm:w-4 sm:h-4 flex-shrink-0" />
<span>TERMS</span>
</Link>
<Link
to="/privacy"
className="flex items-center space-x-1 sm:space-x-2 text-muted-foreground hover:text-foreground"
>
<Shield className="w-3 h-3 sm:w-4 sm:h-4 flex-shrink-0" />
<span>PRIVACY</span>
</Link>
<a
href="https://github.com/waku-org/opchan/"
target="_blank"
rel="noopener noreferrer"
className="flex items-center space-x-1 sm:space-x-2 text-muted-foreground hover:text-foreground"
>
<Github className="w-3 h-3 sm:w-4 sm:h-4 flex-shrink-0" />
<span>GITHUB</span>
</a>
<a
href="https://docs.waku.org"
target="_blank"
rel="noopener noreferrer"
className="flex items-center space-x-1 sm:space-x-2 text-muted-foreground hover:text-foreground"
>
<BookOpen className="w-3 h-3 sm:w-4 sm:h-4 flex-shrink-0" />
<span>DOCS</span>
</a>
</nav>
<div className="text-[9px] sm:text-[10px] uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground text-center">
Reference client · build your own with @opchan/core
</div>
<div className="max-w-6xl mx-auto px-2 py-2">
<div className="text-center text-[10px] text-muted-foreground">
<span>© 2025 OPCHAN</span>
<span className="mx-1">|</span>
<Link to="/terms" className="hover:text-foreground">TERMS</Link>
<span className="mx-1">|</span>
<Link to="/privacy" className="hover:text-foreground">PRIVACY</Link>
<span className="mx-1">|</span>
<a href="https://github.com/waku-org/opchan/" target="_blank" rel="noopener noreferrer" className="hover:text-foreground">GITHUB</a>
<span className="mx-1">|</span>
<a href="https://docs.waku.org" target="_blank" rel="noopener noreferrer" className="hover:text-foreground">DOCS</a>
<span className="mx-1">|</span>
<span>Reference client</span>
</div>
</div>
</footer>

View File

@ -3,26 +3,14 @@ import { Link, useLocation } from 'react-router-dom';
import { useAuth, useForum, useNetwork, useUIState } from '@/hooks';
import { EVerificationStatus } from '@opchan/core';
import { localDatabase } from '@opchan/core';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
LogOut,
Terminal,
AlertTriangle,
CheckCircle,
Key,
CircleSlash,
Home,
Grid3X3,
User,
Bookmark,
Settings,
Menu,
X,
Clock,
Trash2,
Loader2,
} from 'lucide-react';
import {
DropdownMenu,
@ -31,12 +19,6 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import {
AlertDialog,
AlertDialogAction,
@ -53,7 +35,6 @@ import { useEthereumWallet } from '@opchan/react';
import { WalletWizard } from '@/components/ui/wallet-wizard';
import { CallSignSetupDialog } from '@/components/ui/call-sign-setup-dialog';
import { WakuHealthDot } from '@/components/ui/waku-health-indicator';
const Header = () => {
const { currentUser, delegationInfo } = useAuth();
@ -61,12 +42,10 @@ const Header = () => {
const location = useLocation();
const { toast } = useToast();
const { content } = useForum();
const { isConnected, disconnect } = useEthereumWallet();
const [walletWizardOpen, setWalletWizardOpen] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [callSignDialogOpen, setCallSignDialogOpen] = useState(false);
// Use centralized UI state instead of direct LocalDatabase access
@ -152,177 +131,73 @@ const Header = () => {
return (
<>
<header className="bg-cyber-dark border-b border-border sticky top-0 z-40">
<div className="max-w-6xl mx-auto px-3 sm:px-4">
{/* Top Row - Logo, Network Status, User Actions */}
<div className="flex items-center justify-between h-12 sm:h-14 md:h-16">
{/* Left: Logo */}
<div className="flex items-center min-w-0">
<Link
to="/"
className="flex items-center space-x-1 sm:space-x-2 text-xs sm:text-sm font-mono font-semibold uppercase tracking-[0.3em] sm:tracking-[0.4em] text-foreground truncate"
>
<Terminal className="w-4 h-4 sm:w-5 sm:h-5 flex-shrink-0" />
<span className="truncate">opchan</span>
<div className="max-w-6xl mx-auto px-2 py-2">
{/* Single Row - Logo, Nav, Status, User */}
<div className="flex items-center justify-between text-xs gap-2">
{/* Logo & Nav */}
<div className="flex items-center gap-3">
<Link to="/" className="font-semibold text-foreground">
OPCHAN
</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 border border-border text-[10px] uppercase tracking-[0.2em]">
<WakuHealthDot />
<span className="text-[10px] text-muted-foreground">
{statusMessage}
</span>
{syncStatus === 'syncing' && syncDetail && syncDetail.missing > 0 && (
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center space-x-1 text-[10px] text-yellow-400 cursor-help">
<Loader2 className="w-3 h-3 animate-spin" />
<span>SYNCING ({syncDetail.missing})</span>
</div>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">
<strong>Syncing messages</strong>
<br />
Pending: {syncDetail.missing}
<br />
Received: {syncDetail.received}
{syncDetail.lost > 0 && (
<>
<br />
Lost: {syncDetail.lost}
</>
)}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{content.lastSync && (
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center space-x-1 text-[10px] text-muted-foreground cursor-help">
<Clock className="w-3 h-3" />
<span>
{new Date(content.lastSync).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
</span>
</div>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">Last message sync time</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
</div>
{/* Right: User Actions */}
<div className="flex items-center space-x-2 sm:space-x-3 flex-shrink-0">
{/* Network Status (Mobile) */}
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<div className="lg:hidden flex items-center space-x-1 cursor-help">
{syncStatus === 'syncing' && syncDetail && syncDetail.missing > 0 ? (
<Loader2 className="w-4 h-4 text-yellow-400 animate-spin" />
) : (
<WakuHealthDot />
)}
</div>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">
{syncStatus === 'syncing' && syncDetail && syncDetail.missing > 0
? `Syncing ${syncDetail.missing} messages...`
: 'Network connected'}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{/* User Status & Actions */}
{isConnected || currentUser?.verificationStatus === EVerificationStatus.ANONYMOUS ? (
<div className="flex items-center space-x-1 sm:space-x-2">
{/* Status Badge - hidden for anonymous sessions */}
{currentUser?.verificationStatus !== EVerificationStatus.ANONYMOUS && (
<Badge
variant="outline"
className={`hidden sm:flex items-center gap-1 text-[9px] sm:text-[10px] px-1.5 sm:px-2 py-0.5 ${
currentUser?.verificationStatus ===
EVerificationStatus.ENS_VERIFIED &&
delegationInfo?.isValid
? 'border-green-500 text-green-300'
: currentUser?.verificationStatus ===
EVerificationStatus.ENS_VERIFIED
? 'border-orange-500 text-orange-300'
: 'border-yellow-500 text-yellow-300'
}`}
<nav className="hidden sm:flex items-center gap-2">
<Link
to="/"
className={location.pathname === '/' ? 'text-primary' : 'text-muted-foreground hover:text-foreground'}
>
HOME
</Link>
<span className="text-muted-foreground">|</span>
<Link
to="/cells"
className={location.pathname === '/cells' ? 'text-primary' : 'text-muted-foreground hover:text-foreground'}
>
CELLS
</Link>
{isConnected && (
<>
<span className="text-muted-foreground">|</span>
<Link
to="/bookmarks"
className={location.pathname === '/bookmarks' ? 'text-primary' : 'text-muted-foreground hover:text-foreground'}
>
{getStatusIcon()}
<span className="hidden md:inline">
{currentUser?.verificationStatus ===
EVerificationStatus.WALLET_UNCONNECTED
? 'CONNECT'
: delegationInfo?.isValid
? 'READY'
: currentUser?.verificationStatus ===
EVerificationStatus.ENS_VERIFIED
? 'EXPIRED'
: 'DELEGATE'}
</span>
</Badge>
)}
BOOKMARKS
</Link>
</>
)}
</nav>
</div>
{/* Network Status */}
<div className="hidden md:flex items-center gap-2 text-[10px] text-muted-foreground">
<span>{statusMessage}</span>
{syncStatus === 'syncing' && syncDetail && syncDetail.missing > 0 && (
<span className="text-yellow-400">SYNCING ({syncDetail.missing})</span>
)}
</div>
{/* User */}
<div className="flex items-center gap-2">
{isConnected || currentUser?.verificationStatus === EVerificationStatus.ANONYMOUS ? (
<div className="flex items-center gap-2">
{/* User Dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="flex items-center space-x-1 sm:space-x-2 text-foreground border-border px-2 sm:px-3"
>
<div className="text-[10px] sm:text-[11px] uppercase tracking-[0.15em] sm:tracking-[0.2em] truncate max-w-[80px] sm:max-w-none">
{currentUser?.displayName}
</div>
<Settings className="w-3 h-3 sm:w-4 sm:h-4 flex-shrink-0" />
</Button>
<button className="text-foreground hover:text-primary text-[10px]">
{currentUser?.displayName}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-56 bg-[#050505] border border-border text-sm"
>
<DropdownMenuContent align="end" className="w-48 bg-[#050505] border border-border text-xs">
<DropdownMenuItem asChild>
<Link
to="/profile"
className="flex items-center space-x-2"
>
<User className="w-4 h-4" />
<span>Profile</span>
</Link>
<Link to="/profile">Profile</Link>
</DropdownMenuItem>
{currentUser?.verificationStatus === EVerificationStatus.ANONYMOUS ? (
<DropdownMenuItem
onClick={() => setCallSignDialogOpen(true)}
className="flex items-center space-x-2"
>
<User className="w-4 h-4" />
<span>{currentUser?.callSign ? 'Update' : 'Set'} Call Sign</span>
<DropdownMenuItem onClick={() => setCallSignDialogOpen(true)}>
{currentUser?.callSign ? 'Update' : 'Set'} Call Sign
</DropdownMenuItem>
) : (
<DropdownMenuItem
onClick={handleOpenWizard}
className="flex items-center space-x-2"
>
<Settings className="w-4 h-4" />
<span>Setup Wizard</span>
<DropdownMenuItem onClick={handleOpenWizard}>
Setup Wizard
</DropdownMenuItem>
)}
@ -330,12 +205,8 @@ const Header = () => {
<AlertDialog>
<AlertDialogTrigger asChild>
<DropdownMenuItem
onSelect={e => e.preventDefault()}
className="flex items-center space-x-2 text-orange-400 focus:text-orange-400"
>
<Trash2 className="w-4 h-4" />
<span>Clear Database</span>
<DropdownMenuItem onSelect={e => e.preventDefault()} className="text-orange-400 focus:text-orange-400">
Clear Database
</DropdownMenuItem>
</AlertDialogTrigger>
<AlertDialogContent className="bg-[#050505] border border-border text-foreground">
@ -369,181 +240,22 @@ const Header = () => {
</AlertDialogContent>
</AlertDialog>
<DropdownMenuItem
onClick={handleDisconnect}
className="flex items-center space-x-2 text-red-400 focus:text-red-400"
>
<LogOut className="w-4 h-4" />
<span>{currentUser?.verificationStatus === EVerificationStatus.ANONYMOUS ? 'Exit Anonymous' : 'Disconnect'}</span>
<DropdownMenuItem onClick={handleDisconnect} className="text-red-400 focus:text-red-400">
{currentUser?.verificationStatus === EVerificationStatus.ANONYMOUS ? 'Exit Anonymous' : 'Disconnect'}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
) : (
<Button
<button
onClick={handleConnect}
className="text-primary border-primary hover:bg-primary/10 text-[10px] sm:text-[11px] px-2 sm:px-3"
className="text-primary hover:underline text-[10px]"
>
<span className="hidden sm:inline">Connect</span>
<span className="sm:hidden">CON</span>
</Button>
CONNECT
</button>
)}
{/* Mobile Menu Toggle */}
<Button
variant="ghost"
size="sm"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="md:hidden border-border text-foreground p-2"
>
{mobileMenuOpen ? (
<X className="w-4 h-4 sm:w-5 sm:h-5" />
) : (
<Menu className="w-4 h-4 sm:w-5 sm:h-5" />
)}
</Button>
</div>
</div>
{/* Builder Banner */}
<div className="mt-1 mb-1 flex items-center justify-between gap-2 text-[10px] sm:text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
<span className="truncate">
Reference client on top of @opchan/core. UI is intentionally bare.
</span>
<a
href="https://github.com/waku-org/opchan"
target="_blank"
rel="noopener noreferrer"
className="flex-shrink-0 text-primary hover:underline"
>
Build your own
</a>
</div>
{/* Navigation Bar (Desktop) */}
<div className="hidden md:flex items-center justify-center border-t border-border py-2">
<nav className="flex items-center space-x-0.5 text-[11px] uppercase tracking-[0.2em]">
<Link
to="/"
className={`flex items-center space-x-2 px-4 py-2 border-b ${
location.pathname === '/'
? 'border-primary text-primary'
: 'border-transparent text-muted-foreground hover:text-foreground'
}`}
>
<Home className="w-4 h-4" />
<span>HOME</span>
</Link>
<Link
to="/cells"
className={`flex items-center space-x-2 px-4 py-2 border-b ${
location.pathname === '/cells'
? 'border-primary text-primary'
: 'border-transparent text-muted-foreground hover:text-foreground'
}`}
>
<Grid3X3 className="w-4 h-4" />
<span>CELLS</span>
</Link>
{isConnected && (
<>
<Link
to="/bookmarks"
className={`flex items-center space-x-2 px-4 py-2 border-b ${
location.pathname === '/bookmarks'
? 'border-primary text-primary'
: 'border-transparent text-muted-foreground hover:text-foreground'
}`}
>
<Bookmark className="w-4 h-4" />
<span>BOOKMARKS</span>
</Link>
</>
)}
</nav>
</div>
{/* Mobile Navigation */}
{mobileMenuOpen && (
<div className="md:hidden border-t border-border py-4 space-y-2">
<nav className="space-y-1">
<Link
to="/"
className={`flex items-center space-x-3 px-4 py-3 border ${
location.pathname === '/'
? 'border-primary text-primary'
: 'border-border text-muted-foreground'
}`}
onClick={() => setMobileMenuOpen(false)}
>
<Home className="w-4 h-4" />
<span>HOME</span>
</Link>
<Link
to="/cells"
className={`flex items-center space-x-3 px-4 py-3 border ${
location.pathname === '/cells'
? 'border-primary text-primary'
: 'border-border text-muted-foreground'
}`}
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 border ${
location.pathname === '/bookmarks'
? 'border-primary text-primary'
: 'border-border text-muted-foreground'
}`}
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 border ${
location.pathname === '/profile'
? 'border-primary text-primary'
: 'border-border text-muted-foreground'
}`}
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-border">
<div className="flex items-center space-x-2 text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
<WakuHealthDot />
<span>{statusMessage}</span>
{syncStatus === 'syncing' && syncDetail && syncDetail.missing > 0 && (
<span className="text-yellow-400 flex items-center space-x-1">
<Loader2 className="w-3 h-3 animate-spin" />
<span>SYNCING ({syncDetail.missing})</span>
</span>
)}
{content.lastSync && (
<span className="ml-auto" title="Last message sync">
{new Date(content.lastSync).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
</span>
)}
</div>
</div>
</div>
)}
</div>
</header>

View File

@ -1,14 +1,8 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowUp, ArrowDown, MessageSquare } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import type { Post } from '@opchan/core';
import { RelevanceIndicator } from '@/components/ui/relevance-indicator';
import { AuthorDisplay } from '@/components/ui/author-display';
import { BookmarkButton } from '@/components/ui/bookmark-button';
import { LinkRenderer } from '@/components/ui/link-renderer';
import { useAuth, useContent, usePermissions } from '@/hooks';
import { ShareButton } from '@/components/ui/ShareButton';
interface PostCardProps {
post: Post;
@ -72,99 +66,72 @@ const PostCard: React.FC<PostCardProps> = ({ post }) => {
};
return (
<div className="border-b border-border/30 py-3 px-2 hover:bg-border/5">
<div className="flex gap-3">
{/* Vote column - compact */}
<div className="flex flex-col items-center gap-0.5 text-xs min-w-[40px]">
<button
className={`hover:text-primary ${
userUpvoted ? 'text-primary' : 'text-muted-foreground'
}`}
onClick={e => handleVote(e, true)}
disabled={!permissions.canVote}
title={permissions.canVote ? 'Upvote' : permissions.reasons.vote}
>
</button>
<span className={`font-mono text-xs ${score > 0 ? 'text-primary' : score < 0 ? 'text-red-400' : 'text-muted-foreground'}`}>
{score}
</span>
<button
className={`hover:text-blue-400 ${
userDownvoted ? 'text-blue-400' : 'text-muted-foreground'
}`}
onClick={e => handleVote(e, false)}
disabled={!permissions.canVote}
title={permissions.canVote ? 'Downvote' : permissions.reasons.vote}
>
</button>
</div>
<div className="border-b border-border/30 py-1.5 px-2 text-xs">
<div className="flex items-start gap-2">
{/* Inline vote display */}
<button
className={`${userUpvoted ? 'text-primary' : 'text-muted-foreground'} hover:text-primary`}
onClick={e => handleVote(e, true)}
disabled={!permissions.canVote}
title={permissions.canVote ? 'Upvote' : permissions.reasons.vote}
>
</button>
<span className={`font-mono text-xs min-w-[2ch] text-center ${score > 0 ? 'text-primary' : score < 0 ? 'text-red-400' : 'text-muted-foreground'}`}>
{score}
</span>
<button
className={`${userDownvoted ? 'text-blue-400' : 'text-muted-foreground'} hover:text-blue-400`}
onClick={e => handleVote(e, false)}
disabled={!permissions.canVote}
title={permissions.canVote ? 'Downvote' : permissions.reasons.vote}
>
</button>
{/* Content */}
<div className="flex-1 min-w-0 text-xs">
{/* Title */}
<Link to={`/post/${post.id}`} className="block mb-1">
<h2 className="text-sm font-semibold text-foreground hover:underline break-words">
{post.title}
</h2>
</Link>
{/* Metadata line */}
<div className="flex flex-wrap items-center gap-1 text-[11px] text-muted-foreground mb-2">
{/* Content - all inline */}
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-baseline gap-1">
<Link
to={cellName ? `/cell/${post.cellId}` : '#'}
className="text-primary hover:underline"
className="text-primary hover:underline text-[10px]"
onClick={e => {
if (!cellName) e.preventDefault();
}}
>
r/{cellName}
</Link>
<span>·</span>
<AuthorDisplay
address={post.author}
className="text-[11px]"
showBadge={false}
/>
<span>·</span>
<span className="text-muted-foreground/80">
<span className="text-muted-foreground">·</span>
<Link to={`/post/${post.id}`} className="text-foreground hover:underline font-medium">
{post.title}
</Link>
<span className="text-muted-foreground text-[10px]">
by {post.author.slice(0, 6)}...{post.author.slice(-4)}
</span>
<span className="text-muted-foreground text-[10px]">·</span>
<span className="text-muted-foreground text-[10px]">
{formatDistanceToNow(new Date(post.timestamp), {
addSuffix: true,
})}
</span>
{isPending && (
<>
<span>·</span>
<span className="text-yellow-400 text-[10px]">syncing</span>
</>
)}
</div>
{/* Content preview */}
{contentPreview && (
<p className="text-muted-foreground text-xs leading-relaxed mb-2">
<LinkRenderer text={contentPreview} />
</p>
)}
{/* Actions */}
<div className="flex items-center gap-3 text-[11px] text-muted-foreground">
<Link to={`/post/${post.id}`} className="hover:underline">
<span className="text-muted-foreground text-[10px]">·</span>
<Link to={`/post/${post.id}`} className="text-muted-foreground hover:underline text-[10px]">
{commentCount} {commentCount === 1 ? 'reply' : 'replies'}
</Link>
<span className="text-muted-foreground text-[10px]">·</span>
<button
onClick={handleBookmark}
disabled={bookmarkLoading}
className="hover:underline"
className="text-muted-foreground hover:underline text-[10px]"
>
{isBookmarked ? 'unsave' : 'save'}
</button>
<ShareButton
size="sm"
url={`${window.location.origin}/post/${post.id}`}
title={post.title}
/>
{isPending && (
<>
<span className="text-muted-foreground text-[10px]">·</span>
<span className="text-yellow-400 text-[10px]">syncing</span>
</>
)}
</div>
</div>
</div>

View File

@ -9,28 +9,17 @@ import type {
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Skeleton } from '@/components/ui/skeleton';
import { LinkRenderer } from '@/components/ui/link-renderer';
import { RelevanceIndicator } from '@/components/ui/relevance-indicator';
import { ShareButton } from '@/components/ui/ShareButton';
import {
ArrowLeft,
MessageSquare,
MessageCircle,
ArrowUp,
ArrowDown,
RefreshCw,
MessageSquareX,
UserX,
} from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { CypherImage } from './ui/CypherImage';
import { AuthorDisplay } from './ui/author-display';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { InlineCallSignInput } from './ui/inline-callsign-input';
import { EVerificationStatus } from '@opchan/core';
@ -48,55 +37,14 @@ const PostList = () => {
const [newPostTitle, setNewPostTitle] = useState('');
const [newPostContent, setNewPostContent] = useState('');
if (!cellId) {
if (!cellId || !cell) {
return (
<div className="container mx-auto px-4 py-8 max-w-4xl">
<div className="mb-6">
<Link
to="/"
className="text-cyber-accent hover:underline flex items-center gap-1 text-sm"
>
<ArrowLeft className="w-4 h-4" /> Back to Cells
</Link>
</div>
<Skeleton className="h-8 w-32 mb-6 bg-cyber-muted" />
<Skeleton className="h-6 w-64 mb-6 bg-cyber-muted" />
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<div key={i} className="border border-cyber-muted rounded-sm p-4">
<div className="mb-2">
<Skeleton className="h-6 w-full mb-2 bg-cyber-muted" />
<Skeleton className="h-6 w-3/4 mb-2 bg-cyber-muted" />
</div>
<Skeleton className="h-4 w-32 bg-cyber-muted" />
</div>
))}
</div>
</div>
);
}
if (!cell) {
return (
<div className="container mx-auto px-4 py-8 max-w-4xl">
<div className="mb-6">
<Link
to="/"
className="text-cyber-accent hover:underline flex items-center gap-1 text-sm"
>
<ArrowLeft className="w-4 h-4" /> Back to Cells
</Link>
</div>
<div className="p-8 text-center">
<h1 className="text-2xl font-bold mb-4">Cell Not Found</h1>
<p className="text-cyber-neutral mb-6">
The cell you're looking for doesn't exist.
</p>
<Button asChild>
<Link to="/">Return to Cells</Link>
</Button>
<div className="w-full mx-auto px-2 py-2 max-w-4xl">
<Link to="/" className="text-primary hover:underline text-xs">
Back
</Link>
<div className="py-4 text-xs text-muted-foreground">
{!cellId ? 'Loading...' : 'Cell not found'}
</div>
</div>
);
@ -181,236 +129,131 @@ const PostList = () => {
};
return (
<div className="page-main">
<div className="content-spacing">
<Link
to="/"
className="text-cyber-accent hover:underline flex items-center gap-1 text-sm"
>
<ArrowLeft className="w-4 h-4" /> Back to Cells
</Link>
</div>
<div className="flex gap-4 items-start content-spacing">
<CypherImage
src={cell.icon}
alt={cell.name}
className="w-12 h-12 object-cover rounded-sm border border-cyber-muted"
generateUniqueFallback={true}
/>
<div className="flex-1">
<div className="flex items-center justify-between">
<h1 className="page-title text-glow">{cell.name}</h1>
<Button
variant="outline"
size="icon"
onClick={refresh}
disabled={false}
title="Refresh data"
>
<RefreshCw className="w-4 h-4" />
</Button>
</div>
<p className="page-subtitle">{cell.description}</p>
<div className="w-full mx-auto px-2 py-2 max-w-4xl">
<div className="mb-2 pb-1 border-b border-border/30 flex items-center justify-between text-xs">
<div className="flex items-center gap-2">
<Link to="/" className="text-primary hover:underline">
Back
</Link>
<span className="text-muted-foreground">|</span>
<span className="font-semibold text-foreground">r/{cell.name}</span>
<span className="text-muted-foreground text-[10px]">{cell.description}</span>
</div>
<button
onClick={refresh}
className="text-muted-foreground hover:text-foreground text-[10px]"
>
refresh
</button>
</div>
{canPost && (
<div className="section-spacing">
<div className="mb-2 border-b border-border/30 pb-2">
<form onSubmit={handleCreatePost} onKeyDown={handleKeyDown}>
<h2 className="text-sm font-bold mb-2 flex items-center gap-1">
<MessageSquare className="w-4 h-4" />
New Thread
</h2>
<div className="mb-3">
<Input
placeholder="Thread title"
value={newPostTitle}
onChange={e => setNewPostTitle(e.target.value)}
className="mb-3 bg-cyber-muted/50 border-cyber-muted"
disabled={isCreatingPost}
/>
<Textarea
placeholder="What's on your mind?"
value={newPostContent}
onChange={e => setNewPostContent(e.target.value)}
className="bg-cyber-muted/50 border-cyber-muted resize-none"
disabled={isCreatingPost}
/>
</div>
<div className="flex items-center justify-between text-xs text-muted-foreground mt-1 mb-2">
<span>
Press Enter for newline Ctrl+Enter or Shift+Enter to post
</span>
<span />
</div>
<div className="flex justify-end">
<Button
<div className="text-[10px] font-semibold mb-1">NEW THREAD</div>
<Input
placeholder="Title"
value={newPostTitle}
onChange={e => setNewPostTitle(e.target.value)}
className="mb-1 text-xs h-7"
disabled={isCreatingPost}
/>
<Textarea
placeholder="Content"
value={newPostContent}
onChange={e => setNewPostContent(e.target.value)}
className="text-xs resize-none h-16"
disabled={isCreatingPost}
/>
<div className="flex justify-end mt-1">
<button
type="submit"
disabled={
isCreatingPost ||
!newPostContent.trim() ||
!newPostTitle.trim()
}
className="text-primary hover:underline text-[10px] disabled:opacity-50"
>
{isCreatingPost ? 'Posting...' : 'Post Thread'}
</Button>
{isCreatingPost ? 'posting...' : 'post'}
</button>
</div>
</form>
</div>
)}
{/* Inline Call Sign Suggestion for Anonymous Users */}
{currentUser?.verificationStatus === EVerificationStatus.ANONYMOUS && !currentUser.callSign && canPost && (
<div className="section-spacing">
<InlineCallSignInput />
</div>
)}
{!canPost && !currentUser && (
<div className="section-spacing content-card-sm text-center">
<p className="text-sm mb-3">Connect your wallet to post</p>
<Button asChild size="sm">
<Link to="/">Connect Wallet</Link>
</Button>
<div className="mb-2 py-2 text-xs text-center text-muted-foreground border-b border-border/30">
Connect wallet to post
</div>
)}
<div className="space-y-4">
<div>
{visiblePosts.length === 0 ? (
<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 to start a thread.'}
</p>
<div className="py-4 text-xs text-muted-foreground text-center">
No threads yet. {canPost ? 'Be the first to post!' : 'Connect wallet to post.'}
</div>
) : (
visiblePosts.map((post: ForumPost) => (
<div key={post.id} className="thread-card">
<div className="flex gap-4">
<div className="flex flex-col items-center">
<button
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${getPostVoteType(post.id) === 'upvote' ? 'text-cyber-accent' : ''}`}
onClick={() => handleVotePost(post.id, true)}
disabled={!canVote || isVoting}
title={canVote ? 'Upvote' : 'Connect your wallet to vote'}
>
<ArrowUp className="w-4 h-4" />
</button>
<span className="text-sm py-1">
{post.upvotes.length - post.downvotes.length}
</span>
<button
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${getPostVoteType(post.id) === 'downvote' ? 'text-cyber-accent' : ''}`}
onClick={() => handleVotePost(post.id, false)}
disabled={!canVote || isVoting}
title={canVote ? 'Downvote' : 'Connect your wallet to vote'}
>
<ArrowDown className="w-4 h-4" />
</button>
</div>
<div key={post.id} className="border-b border-border/30 py-1.5 text-xs">
<div className="flex items-start gap-2">
<button
className={`${getPostVoteType(post.id) === 'upvote' ? 'text-primary' : 'text-muted-foreground'} hover:text-primary`}
onClick={() => handleVotePost(post.id, true)}
disabled={!canVote || isVoting}
>
</button>
<span className={`font-mono text-xs min-w-[2ch] text-center ${(post.upvotes.length - post.downvotes.length) > 0 ? 'text-primary' : 'text-muted-foreground'}`}>
{post.upvotes.length - post.downvotes.length}
</span>
<button
className={`${getPostVoteType(post.id) === 'downvote' ? 'text-blue-400' : 'text-muted-foreground'} hover:text-blue-400`}
onClick={() => handleVotePost(post.id, false)}
disabled={!canVote || isVoting}
>
</button>
<div className="flex-1">
<Link to={`/post/${post.id}`} className="block">
<h2 className="text-lg font-bold hover:text-cyber-accent">
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-baseline gap-1">
<Link to={`/post/${post.id}`} className="text-foreground hover:underline font-medium">
{post.title}
</h2>
<p className="line-clamp-2 text-sm mb-3">
<LinkRenderer text={post.content} />
</p>
<div className="flex items-center gap-4 text-xs text-cyber-neutral">
<span>
{formatDistanceToNow(post.timestamp, {
addSuffix: true,
})}
</span>
<span>by </span>
<AuthorDisplay
address={post.author}
className="text-xs"
showBadge={false}
/>
<span></span>
<span>
<MessageSquare className="inline w-3 h-3 mr-1" />
{commentsByPost[post.id]?.length || 0} comments
</span>
{typeof post.relevanceScore === 'number' && (
<>
<span></span>
<RelevanceIndicator
score={post.relevanceScore}
details={post.relevanceDetails}
type="post"
showTooltip={true}
/>
</>
)}
<ShareButton
url={`${window.location.origin}/post/${post.id}`}
title={post.title}
description={post.content}
size="sm"
variant="ghost"
className="text-cyber-neutral"
showText={false}
/>
</div>
</Link>
{canModerate(cell.id) && !post.moderated && (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-cyber-neutral hover:text-orange-500"
</Link>
<span className="text-muted-foreground text-[10px]">
by {post.author.slice(0, 6)}...{post.author.slice(-4)}
</span>
<span className="text-muted-foreground text-[10px]">·</span>
<span className="text-muted-foreground text-[10px]">
{formatDistanceToNow(post.timestamp, { addSuffix: true })}
</span>
<span className="text-muted-foreground text-[10px]">·</span>
<Link to={`/post/${post.id}`} className="text-muted-foreground hover:underline text-[10px]">
{commentsByPost[post.id]?.length || 0} comments
</Link>
{canModerate(cell.id) && !post.moderated && (
<>
<span className="text-muted-foreground text-[10px]">·</span>
<button
onClick={() => handleModerate(post.id)}
className="text-orange-400 hover:underline text-[10px]"
>
<MessageSquareX className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Moderate post</p>
</TooltipContent>
</Tooltip>
)}
{canModerate(cell.id) && post.author !== cell.author && (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-cyber-neutral hover:text-red-500"
onClick={() => handleModerateUser(post.author)}
>
<UserX className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Moderate user</p>
</TooltipContent>
</Tooltip>
)}
{canModerate(cell.id) && post.moderated && (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="ghost"
className="h-6 px-2 text-cyber-neutral hover:text-green-500"
moderate
</button>
</>
)}
{canModerate(cell.id) && post.moderated && (
<>
<span className="text-muted-foreground text-[10px]">·</span>
<button
onClick={() => handleUnmoderate(post.id)}
className="text-green-400 hover:underline text-[10px]"
>
Unmoderate
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Unmoderate post</p>
</TooltipContent>
</Tooltip>
)}
unmoderate
</button>
</>
)}
</div>
</div>
</div>
</div>

View File

@ -33,3 +33,5 @@ export const GreentextRenderer: React.FC<GreentextRendererProps> = ({
);
};

View File

@ -139,7 +139,7 @@
}
.page-header {
@apply mb-4 sm:mb-6 border-b border-border pb-3 sm:pb-4;
@apply mb-2 sm:mb-3 border-b border-border pb-2 sm:pb-3;
}
.page-title {
@ -151,53 +151,53 @@
}
.page-content {
@apply flex-1 pt-8 sm:pt-10;
@apply flex-1 pt-4 sm:pt-6;
}
.page-main {
@apply w-full mx-auto px-3 sm:px-4 md:px-8 py-4 sm:py-6 md:py-8 max-w-5xl;
@apply w-full mx-auto px-2 sm:px-3 md:px-4 py-2 sm:py-3 md:py-4 max-w-5xl;
}
.page-footer {
@apply border-t border-border py-2 sm:py-3 text-center text-[10px] text-muted-foreground px-3;
}
/* Card Components */
/* Card Components - Simplified to flat list items */
.card-base {
@apply bg-transparent border border-border/70 rounded-none shadow-none;
@apply bg-transparent border-0 rounded-none shadow-none;
}
.card-hover {
@apply hover:bg-white/5;
@apply hover:bg-transparent;
}
.card-padding {
@apply p-3 sm:p-4;
@apply p-2;
}
.card-padding-sm {
@apply p-2 sm:p-3;
@apply p-1;
}
.thread-card {
@apply card-base card-hover card-padding-sm mb-3;
@apply border-b border-border/30 py-2 mb-0;
}
.board-card {
@apply card-base card-hover card-padding-sm mb-2;
@apply border-b border-border/30 py-2 mb-0;
}
.comment-card {
@apply border-l border-border pl-3 py-2 my-2;
@apply border-l border-border pl-2 py-1 my-1;
}
/* Content Cards */
.content-card {
@apply card-base card-hover card-padding;
@apply border-b border-border/30 py-2;
}
.content-card-sm {
@apply card-base card-hover card-padding-sm;
@apply border-b border-border/30 py-1;
}
/* Status Indicators */
@ -227,17 +227,17 @@
}
/* Typography */
/* Spacing Utilities */
/* Spacing Utilities - Simplified for raw layout */
.section-spacing {
@apply mb-8;
@apply mb-4;
}
.content-spacing {
@apply mb-6;
@apply mb-3;
}
.item-spacing {
@apply mb-4;
@apply mb-2;
}
/* Grid Layouts */
@ -253,21 +253,21 @@
@apply lg:col-span-1;
}
/* Empty States */
/* Empty States - Simplified */
.empty-state {
@apply flex flex-col items-center justify-center py-8 sm:py-12 md:py-16 px-3 sm:px-4 text-center;
@apply py-4 px-2 text-center;
}
.empty-state-icon {
@apply w-12 h-12 sm:w-16 sm:h-16 text-cyber-accent/50 mb-3 sm:mb-4;
@apply hidden;
}
.empty-state-title {
@apply text-base sm:text-lg md:text-xl font-mono font-bold text-white mb-2;
@apply text-xs font-mono text-muted-foreground mb-1;
}
.empty-state-description {
@apply text-sm sm:text-base text-cyber-neutral mb-3 sm:mb-4 px-2;
@apply text-xs text-muted-foreground mb-2;
}
}

View File

@ -1,7 +1,4 @@
import React, { useMemo, useState } from 'react';
import { RefreshCw, TrendingUp, Clock } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import {
Select,
SelectContent,
@ -10,7 +7,6 @@ import {
SelectValue,
} from '@/components/ui/select';
import PostCard from '@/components/PostCard';
import FeedSidebar from '@/components/FeedSidebar';
import { ModerationToggle } from '@/components/ui/moderation-toggle';
import { useAuth, useContent, useNetwork } from '@/hooks';
import { EVerificationStatus } from '@opchan/core';
@ -26,7 +22,7 @@ const FeedPage: React.FC = () => {
[content.posts, sortOption]
);
// Loading skeleton - only show if store is not yet hydrated
// Simple loading text
if (
!isHydrated &&
!content.posts.length &&
@ -35,52 +31,8 @@ const FeedPage: React.FC = () => {
) {
return (
<div className="min-h-screen bg-cyber-dark">
<div className="container mx-auto px-3 sm:px-4 py-4 sm:py-6 max-w-6xl">
<div className="flex flex-col lg:flex-row gap-4 lg:gap-6">
{/* Main feed skeleton */}
<div className="flex-1 lg:max-w-3xl">
<div className="space-y-3 sm:space-y-4">
{[...Array(5)].map((_, i) => (
<div
key={i}
className="bg-cyber-muted/20 border border-cyber-muted rounded-sm p-3 sm:p-4"
>
<div className="flex gap-3 sm:gap-4">
<div className="w-8 sm:w-10 space-y-2 flex-shrink-0">
<Skeleton className="h-5 w-5 sm:h-6 sm:w-6" />
<Skeleton className="h-3 w-6 sm:h-4 sm:w-8" />
<Skeleton className="h-5 w-5 sm:h-6 sm:w-6" />
</div>
<div className="flex-1 space-y-2 min-w-0">
<Skeleton className="h-3 sm:h-4 w-2/3" />
<Skeleton className="h-5 sm:h-6 w-full" />
<Skeleton className="h-3 sm:h-4 w-full" />
<Skeleton className="h-3 sm:h-4 w-3/4" />
<Skeleton className="h-3 sm:h-4 w-1/3" />
</div>
</div>
</div>
))}
</div>
</div>
{/* Sidebar skeleton - hidden on mobile */}
<div className="hidden lg:block lg:w-80 space-y-4">
{[...Array(3)].map((_, i) => (
<div
key={i}
className="bg-cyber-muted/20 border border-cyber-muted rounded-sm p-4"
>
<Skeleton className="h-6 w-1/2 mb-3" />
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-1/2" />
</div>
</div>
))}
</div>
</div>
<div className="container mx-auto px-2 py-4 max-w-4xl">
<div className="text-xs text-muted-foreground">Loading...</div>
</div>
</div>
);
@ -88,89 +40,44 @@ const FeedPage: React.FC = () => {
return (
<div className="page-container">
<div className="page-main">
{/* Page Header */}
<div className="page-header">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-0">
<div>
<h1 className="page-title text-primary">
Popular Posts
</h1>
<p className="page-subtitle hidden sm:block">Latest posts from all cells</p>
</div>
<div className="flex flex-wrap items-center gap-2 sm:gap-2">
<ModerationToggle />
<Select
value={sortOption}
onValueChange={(value: SortOption) => setSortOption(value)}
>
<SelectTrigger className="w-32 sm:w-40 text-[11px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="relevance">
<div className="flex items-center gap-2">
<TrendingUp className="w-4 h-4" />
<span>Relevance</span>
</div>
</SelectItem>
<SelectItem value="time">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4" />
<span>Newest</span>
</div>
</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
onClick={content.refresh}
disabled={false}
className="flex items-center space-x-1 sm:space-x-2 text-[11px]"
>
<RefreshCw className="w-3 h-3 sm:w-4 sm:h-4" />
<span className="hidden sm:inline">Refresh</span>
</Button>
</div>
<div className="w-full mx-auto px-2 py-2 max-w-4xl">
{/* Minimal Header */}
<div className="mb-2 pb-1 border-b border-border/30 flex items-center justify-between text-xs">
<div className="flex items-center gap-2">
<span className="text-primary font-semibold">FEED</span>
<ModerationToggle />
</div>
<div className="flex items-center gap-2">
<Select
value={sortOption}
onValueChange={(value: SortOption) => setSortOption(value)}
>
<SelectTrigger className="w-24 text-[10px] h-6">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="relevance">Relevance</SelectItem>
<SelectItem value="time">Newest</SelectItem>
</SelectContent>
</Select>
<button
onClick={content.refresh}
className="text-muted-foreground hover:text-foreground text-[10px]"
>
refresh
</button>
</div>
</div>
<div className="flex flex-col lg:flex-row gap-4 lg:gap-6">
{/* Main Feed */}
<div className="flex-1 lg:max-w-3xl min-w-0">
{/* Posts Feed */}
<div className="space-y-0">
{allPosts.length === 0 ? (
<div className="empty-state">
<div className="space-y-3">
<h3 className="empty-state-title tracking-[0.25em]">
No posts yet
</h3>
<p className="empty-state-description">
This is a reference UI built on top of @opchan/core.
Swap this screen with your own feed layout.
</p>
{verificationStatus !==
EVerificationStatus.ENS_VERIFIED && (
<p className="text-xs sm:text-sm text-cyber-neutral/80">
Connect your wallet to try the reference client or fork it and build your own.
</p>
)}
</div>
</div>
) : (
allPosts.map(post => <PostCard key={post.id} post={post} />)
)}
{/* Posts List */}
<div>
{allPosts.length === 0 ? (
<div className="py-4 text-xs text-muted-foreground text-center">
No posts yet. Connect wallet to post.
</div>
</div>
{/* Sidebar */}
<div className="hidden lg:block lg:w-80 lg:flex-shrink-0">
<FeedSidebar />
</div>
) : (
allPosts.map(post => <PostCard key={post.id} post={post} />)
)}
</div>
</div>
</div>