mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 21:03:09 +00:00
feat: add feed-UI
This commit is contained in:
parent
95f168894b
commit
d4363da79a
@ -22,6 +22,7 @@ import CellPage from "./pages/CellPage";
|
||||
import PostPage from "./pages/PostPage";
|
||||
import NotFound from "./pages/NotFound";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
import Index from "./pages/Index";
|
||||
import { appkitConfig } from "./lib/identity/wallets/appkit";
|
||||
import { createAppKit } from "@reown/appkit";
|
||||
import { WagmiProvider } from "wagmi";
|
||||
@ -45,6 +46,7 @@ const App = () => (
|
||||
<Sonner />
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/cells" element={<Index />} />
|
||||
<Route path="/cell/:cellId" element={<CellPage />} />
|
||||
<Route path="/post/:postId" element={<PostPage />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
|
||||
@ -37,11 +37,19 @@ const formSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export function CreateCellDialog() {
|
||||
interface CreateCellDialogProps {
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function CreateCellDialog({ open: externalOpen, onOpenChange }: CreateCellDialogProps = {}) {
|
||||
const { createCell, isPostingCell } = useForum();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { toast } = useToast();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [internalOpen, setInternalOpen] = React.useState(false);
|
||||
|
||||
const open = externalOpen ?? internalOpen;
|
||||
const setOpen = onOpenChange ?? setInternalOpen;
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
@ -77,9 +85,11 @@ export function CreateCellDialog() {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="w-full">Create New Cell</Button>
|
||||
</DialogTrigger>
|
||||
{!onOpenChange && (
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="w-full">Create New Cell</Button>
|
||||
</DialogTrigger>
|
||||
)}
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create a New Cell</DialogTitle>
|
||||
@ -151,3 +161,5 @@ export function CreateCellDialog() {
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateCellDialog;
|
||||
|
||||
219
src/components/FeedSidebar.tsx
Normal file
219
src/components/FeedSidebar.tsx
Normal file
@ -0,0 +1,219 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Plus, TrendingUp, Users, Eye } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useForum } from '@/contexts/useForum';
|
||||
import { useAuth } from '@/contexts/useAuth';
|
||||
import { CypherImage } from '@/components/ui/CypherImage';
|
||||
import {CreateCellDialog} from '@/components/CreateCellDialog';
|
||||
|
||||
const FeedSidebar: React.FC = () => {
|
||||
const { cells, posts } = useForum();
|
||||
const { currentUser, verificationStatus } = useAuth();
|
||||
const [showCreateCell, setShowCreateCell] = useState(false);
|
||||
|
||||
// Calculate trending cells based on recent post activity
|
||||
const trendingCells = cells
|
||||
.map(cell => {
|
||||
const cellPosts = posts.filter(post => post.cellId === cell.id);
|
||||
const recentPosts = cellPosts.filter(post =>
|
||||
Date.now() - post.timestamp < 24 * 60 * 60 * 1000 // Last 24 hours
|
||||
);
|
||||
const totalScore = cellPosts.reduce((sum, post) =>
|
||||
sum + (post.upvotes.length - post.downvotes.length), 0
|
||||
);
|
||||
|
||||
return {
|
||||
...cell,
|
||||
postCount: cellPosts.length,
|
||||
recentPostCount: recentPosts.length,
|
||||
totalScore,
|
||||
activity: recentPosts.length + (totalScore * 0.1) // Simple activity score
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.activity - a.activity)
|
||||
.slice(0, 5);
|
||||
|
||||
// User's verification status display
|
||||
const getVerificationBadge = () => {
|
||||
if (!currentUser) {
|
||||
return <Badge variant="secondary">Not Connected</Badge>;
|
||||
}
|
||||
|
||||
// Ethereum wallet with ENS
|
||||
if (currentUser.walletType === 'ethereum') {
|
||||
if (currentUser.ensName && verificationStatus === 'verified-owner') {
|
||||
return <Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">✓ Owns ENS: {currentUser.ensName}</Badge>;
|
||||
} else {
|
||||
return <Badge variant="outline">Read-only (No ENS detected)</Badge>;
|
||||
}
|
||||
}
|
||||
|
||||
// Bitcoin wallet with Ordinal
|
||||
if (currentUser.walletType === 'bitcoin') {
|
||||
if (verificationStatus === 'verified-owner') {
|
||||
return <Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">✓ Owns Ordinal</Badge>;
|
||||
} else {
|
||||
return <Badge variant="outline">Read-only (No Ordinal detected)</Badge>;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback cases
|
||||
switch (verificationStatus) {
|
||||
case 'verified-none':
|
||||
return <Badge variant="outline">Read Only</Badge>;
|
||||
case 'verifying':
|
||||
return <Badge variant="outline">Verifying...</Badge>;
|
||||
default:
|
||||
return <Badge variant="secondary">Not Connected</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* User Info Card */}
|
||||
{currentUser && (
|
||||
<Card className="bg-cyber-muted/20 border-cyber-muted">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-cyber-accent">Your Account</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="text-xs text-cyber-neutral">
|
||||
{currentUser.address.slice(0, 8)}...{currentUser.address.slice(-6)}
|
||||
</div>
|
||||
{getVerificationBadge()}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Create Cell */}
|
||||
<Card className="bg-cyber-muted/20 border-cyber-muted">
|
||||
<CardContent className="p-4">
|
||||
<Button
|
||||
onClick={() => setShowCreateCell(true)}
|
||||
className="w-full"
|
||||
disabled={verificationStatus !== 'verified-owner'}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create Cell
|
||||
</Button>
|
||||
{verificationStatus !== 'verified-owner' && (
|
||||
<p className="text-xs text-cyber-neutral mt-2 text-center">
|
||||
{currentUser?.walletType === 'ethereum'
|
||||
? 'Own an ENS name to create cells'
|
||||
: 'Own a Bitcoin Ordinal to create cells'
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Trending Cells */}
|
||||
<Card className="bg-cyber-muted/20 border-cyber-muted">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium flex items-center text-cyber-accent">
|
||||
<TrendingUp className="w-4 h-4 mr-2" />
|
||||
Trending Cells
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{trendingCells.length === 0 ? (
|
||||
<p className="text-xs text-cyber-neutral">No cells yet</p>
|
||||
) : (
|
||||
trendingCells.map((cell, index) => (
|
||||
<Link
|
||||
key={cell.id}
|
||||
to={`/cell/${cell.id}`}
|
||||
className="flex items-center space-x-3 p-2 rounded-sm hover:bg-cyber-muted/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center space-x-2 flex-1 min-w-0">
|
||||
<span className="text-xs font-medium text-cyber-neutral w-4">
|
||||
{index + 1}
|
||||
</span>
|
||||
<CypherImage
|
||||
src={cell.icon}
|
||||
alt={cell.name}
|
||||
className="w-6 h-6 rounded-sm flex-shrink-0"
|
||||
generateUniqueFallback={true}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium text-glow truncate">
|
||||
r/{cell.name}
|
||||
</div>
|
||||
<div className="text-xs text-cyber-neutral">
|
||||
{cell.postCount} posts
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{cell.recentPostCount > 0 && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{cell.recentPostCount} new
|
||||
</Badge>
|
||||
)}
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* All Cells */}
|
||||
<Card className="bg-cyber-muted/20 border-cyber-muted">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium flex items-center text-cyber-accent">
|
||||
<Users className="w-4 h-4 mr-2" />
|
||||
All Cells
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{cells.length === 0 ? (
|
||||
<p className="text-xs text-cyber-neutral">No cells created yet</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{cells.slice(0, 8).map(cell => (
|
||||
<Link
|
||||
key={cell.id}
|
||||
to={`/cell/${cell.id}`}
|
||||
className="block text-sm text-cyber-neutral hover:text-cyber-accent transition-colors"
|
||||
>
|
||||
r/{cell.name}
|
||||
</Link>
|
||||
))}
|
||||
{cells.length > 8 && (
|
||||
<Link
|
||||
to="/"
|
||||
className="block text-xs text-cyber-neutral hover:text-cyber-accent transition-colors"
|
||||
>
|
||||
View all cells →
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* About */}
|
||||
<Card className="bg-cyber-muted/20 border-cyber-muted">
|
||||
<CardContent className="p-4 text-center">
|
||||
<div className="text-xs text-cyber-neutral space-y-1">
|
||||
<p>OpChan v1.0</p>
|
||||
<p>Decentralized forum on Bitcoin Ordinals</p>
|
||||
<div className="flex items-center justify-center space-x-1 mt-2">
|
||||
<Eye className="w-3 h-3" />
|
||||
<span>Powered by Waku</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Create Cell Dialog */}
|
||||
<CreateCellDialog
|
||||
open={showCreateCell}
|
||||
onOpenChange={setShowCreateCell}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedSidebar;
|
||||
@ -1,10 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '@/contexts/useAuth';
|
||||
import { useForum } from '@/contexts/useForum';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { LogOut, Terminal, Wifi, WifiOff, AlertTriangle, CheckCircle, Key, RefreshCw, CircleSlash } from 'lucide-react';
|
||||
import { LogOut, Terminal, Wifi, WifiOff, AlertTriangle, CheckCircle, Key, RefreshCw, CircleSlash, Home, Grid3X3, Plus } from 'lucide-react';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { useAppKitAccount, useDisconnect } from '@reown/appkit/react';
|
||||
@ -22,6 +22,7 @@ const Header = () => {
|
||||
isWalletAvailable
|
||||
} = useAuth();
|
||||
const { isNetworkConnected, isRefreshing } = useForum();
|
||||
const location = useLocation();
|
||||
const { toast } = useToast();
|
||||
// Use AppKit hooks for multi-chain support
|
||||
const bitcoinAccount = useAppKitAccount({ namespace: "bip122" });
|
||||
@ -125,12 +126,39 @@ const Header = () => {
|
||||
<>
|
||||
<header className="border-b border-cyber-muted bg-cyber-dark fixed top-0 left-0 right-0 z-50 h-16">
|
||||
<div className="container mx-auto px-4 h-full flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="text-cyber-accent w-6 h-6" />
|
||||
<Link to="/" className="text-xl font-bold text-glow text-cyber-accent">
|
||||
OpChan
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="text-cyber-accent w-6 h-6" />
|
||||
<Link to="/" className="text-xl font-bold text-glow text-cyber-accent">
|
||||
OpChan
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<nav className="hidden md:flex items-center space-x-1">
|
||||
<Link
|
||||
to="/"
|
||||
className={`flex items-center space-x-2 px-3 py-2 text-sm font-medium rounded-sm transition-colors ${
|
||||
location.pathname === '/'
|
||||
? 'bg-cyber-accent/20 text-cyber-accent'
|
||||
: 'text-gray-300 hover:text-cyber-accent hover:bg-cyber-accent/10'
|
||||
}`}
|
||||
>
|
||||
<Home className="w-4 h-4" />
|
||||
<span>Feed</span>
|
||||
</Link>
|
||||
<Link
|
||||
to="/cells"
|
||||
className={`flex items-center space-x-2 px-3 py-2 text-sm font-medium rounded-sm transition-colors ${
|
||||
location.pathname === '/cells'
|
||||
? 'bg-cyber-accent/20 text-cyber-accent'
|
||||
: 'text-gray-300 hover:text-cyber-accent hover:bg-cyber-accent/10'
|
||||
}`}
|
||||
>
|
||||
<Grid3X3 className="w-4 h-4" />
|
||||
<span>Cells</span>
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 items-center">
|
||||
|
||||
117
src/components/PostCard.tsx
Normal file
117
src/components/PostCard.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ArrowUp, ArrowDown, MessageSquare } from 'lucide-react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { Post } from '@/types';
|
||||
import { useForum } from '@/contexts/useForum';
|
||||
import { useAuth } from '@/contexts/useAuth';
|
||||
|
||||
interface PostCardProps {
|
||||
post: Post;
|
||||
commentCount?: number;
|
||||
}
|
||||
|
||||
const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => {
|
||||
const { getCellById, votePost, isVoting } = useForum();
|
||||
const { isAuthenticated, currentUser } = useAuth();
|
||||
|
||||
const cell = getCellById(post.cellId);
|
||||
const cellName = cell?.name || 'unknown';
|
||||
|
||||
// Calculate vote score
|
||||
const score = post.upvotes.length - post.downvotes.length;
|
||||
|
||||
// Check user's vote status
|
||||
const userUpvoted = currentUser ? post.upvotes.some(vote => vote.author === currentUser.address) : false;
|
||||
const userDownvoted = currentUser ? post.downvotes.some(vote => vote.author === currentUser.address) : false;
|
||||
|
||||
// Truncate content for preview
|
||||
const contentPreview = post.content.length > 200
|
||||
? post.content.substring(0, 200) + '...'
|
||||
: post.content;
|
||||
|
||||
const handleVote = async (e: React.MouseEvent, isUpvote: boolean) => {
|
||||
e.preventDefault(); // Prevent navigation when clicking vote buttons
|
||||
if (!isAuthenticated) return;
|
||||
await votePost(post.id, isUpvote);
|
||||
};
|
||||
|
||||
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="flex">
|
||||
{/* Voting column */}
|
||||
<div className="flex flex-col items-center p-2 bg-cyber-muted/50 border-r border-cyber-muted">
|
||||
<button
|
||||
className={`p-1 rounded hover:bg-cyber-muted transition-colors ${
|
||||
userUpvoted ? 'text-cyber-accent' : 'text-cyber-neutral hover:text-cyber-accent'
|
||||
}`}
|
||||
onClick={(e) => handleVote(e, true)}
|
||||
disabled={!isAuthenticated || isVoting}
|
||||
title={isAuthenticated ? "Upvote" : "Connect wallet to vote"}
|
||||
>
|
||||
<ArrowUp className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<span className={`text-sm font-medium px-1 ${
|
||||
score > 0 ? 'text-cyber-accent' :
|
||||
score < 0 ? 'text-blue-400' :
|
||||
'text-cyber-neutral'
|
||||
}`}>
|
||||
{score}
|
||||
</span>
|
||||
|
||||
<button
|
||||
className={`p-1 rounded hover:bg-cyber-muted transition-colors ${
|
||||
userDownvoted ? 'text-blue-400' : 'text-cyber-neutral hover:text-blue-400'
|
||||
}`}
|
||||
onClick={(e) => handleVote(e, false)}
|
||||
disabled={!isAuthenticated || isVoting}
|
||||
title={isAuthenticated ? "Downvote" : "Connect wallet to vote"}
|
||||
>
|
||||
<ArrowDown className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content column */}
|
||||
<div className="flex-1 p-3">
|
||||
<Link to={`/post/${post.id}`} className="block hover:opacity-80">
|
||||
{/* Post metadata */}
|
||||
<div className="flex items-center text-xs text-cyber-neutral mb-2 space-x-2">
|
||||
<span className="font-medium text-cyber-accent">r/{cellName}</span>
|
||||
<span>•</span>
|
||||
<span>Posted by u/{post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)}</span>
|
||||
<span>•</span>
|
||||
<span>{formatDistanceToNow(new Date(post.timestamp), { addSuffix: true })}</span>
|
||||
</div>
|
||||
|
||||
{/* Post title */}
|
||||
<h2 className="text-lg font-semibold text-glow mb-2 hover:text-cyber-accent transition-colors">
|
||||
{post.title}
|
||||
</h2>
|
||||
|
||||
{/* Post content preview */}
|
||||
<p className="text-cyber-neutral text-sm leading-relaxed mb-3">
|
||||
{contentPreview}
|
||||
</p>
|
||||
|
||||
{/* Post actions */}
|
||||
<div className="flex items-center space-x-4 text-xs text-cyber-neutral">
|
||||
<div className="flex items-center space-x-1 hover:text-cyber-accent transition-colors">
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
<span>{commentCount} comments</span>
|
||||
</div>
|
||||
<button className="hover:text-cyber-accent transition-colors">
|
||||
Share
|
||||
</button>
|
||||
<button className="hover:text-cyber-accent transition-colors">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostCard;
|
||||
@ -82,9 +82,9 @@ export function WalletWizard({
|
||||
return "pending";
|
||||
}
|
||||
|
||||
// Step 3: Key delegation - completed when delegation is valid
|
||||
// Step 3: Key delegation - completed when delegation is valid AND authenticated
|
||||
if (step === 3) {
|
||||
if (isDelegationValid()) return "completed";
|
||||
if (isAuthenticated && isDelegationValid()) return "completed";
|
||||
if (currentStep === step) return "current";
|
||||
return "pending";
|
||||
}
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import React from 'react';
|
||||
import Index from './Index';
|
||||
import ActivityFeed from '@/components/ActivityFeed';
|
||||
import Header from '@/components/Header';
|
||||
import FeedPage from './FeedPage';
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
return (
|
||||
<div className="pt-16">
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<ActivityFeed />
|
||||
<Index />
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-screen flex flex-col bg-cyber-dark">
|
||||
<Header />
|
||||
<main className="flex-1 pt-16">
|
||||
<FeedPage />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
151
src/pages/FeedPage.tsx
Normal file
151
src/pages/FeedPage.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { RefreshCw, Plus } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import PostCard from '@/components/PostCard';
|
||||
import FeedSidebar from '@/components/FeedSidebar';
|
||||
import { useForum } from '@/contexts/useForum';
|
||||
import { useAuth } from '@/contexts/useAuth';
|
||||
|
||||
const FeedPage: React.FC = () => {
|
||||
const {
|
||||
posts,
|
||||
comments,
|
||||
isInitialLoading,
|
||||
isRefreshing,
|
||||
refreshData
|
||||
} = useForum();
|
||||
const { verificationStatus } = useAuth();
|
||||
|
||||
// Combine posts from all cells and sort by timestamp (newest first)
|
||||
const allPosts = useMemo(() => {
|
||||
return [...posts]
|
||||
.sort((a, b) => b.timestamp - a.timestamp)
|
||||
.filter(post => !post.moderated); // Hide moderated posts from main feed
|
||||
}, [posts]);
|
||||
|
||||
// Calculate comment counts for each post
|
||||
const getCommentCount = (postId: string) => {
|
||||
return comments.filter(comment => comment.postId === postId && !comment.moderated).length;
|
||||
};
|
||||
|
||||
// Loading skeleton
|
||||
if (isInitialLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-cyber-dark">
|
||||
<div className="container mx-auto px-4 py-6 max-w-6xl">
|
||||
<div className="flex gap-6">
|
||||
{/* Main feed skeleton */}
|
||||
<div className="flex-1 max-w-3xl">
|
||||
<div className="space-y-4">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div key={i} className="bg-cyber-muted/20 border border-cyber-muted rounded-sm p-4">
|
||||
<div className="flex gap-4">
|
||||
<div className="w-10 space-y-2">
|
||||
<Skeleton className="h-6 w-6" />
|
||||
<Skeleton className="h-4 w-8" />
|
||||
<Skeleton className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
<Skeleton className="h-6 w-full" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-4 w-1/3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar skeleton */}
|
||||
<div className="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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-cyber-dark">
|
||||
<div className="container mx-auto px-4 py-6 max-w-6xl">
|
||||
{/* 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="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={refreshData}
|
||||
disabled={isRefreshing}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||
<span>Refresh</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-6">
|
||||
{/* Main Feed */}
|
||||
<div className="flex-1 max-w-3xl">
|
||||
{/* 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="space-y-3">
|
||||
<h3 className="text-lg font-semibold text-glow">
|
||||
No posts yet
|
||||
</h3>
|
||||
<p className="text-cyber-neutral">
|
||||
Be the first to create a post in a cell!
|
||||
</p>
|
||||
{verificationStatus !== 'verified-owner' && (
|
||||
<p className="text-sm text-cyber-neutral/80">
|
||||
Connect your wallet and verify Ordinal ownership to start posting
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
allPosts.map(post => (
|
||||
<PostCard
|
||||
key={post.id}
|
||||
post={post}
|
||||
commentCount={getCommentCount(post.id)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="w-80 flex-shrink-0">
|
||||
<FeedSidebar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedPage;
|
||||
Loading…
x
Reference in New Issue
Block a user