chore: linting + improve CSS

This commit is contained in:
Danish Arora 2025-10-03 19:00:01 +05:30
parent b6e78ac71c
commit f9863121ba
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
70 changed files with 386 additions and 313 deletions

View File

@ -1,5 +1,5 @@
import React from 'react'; import React, { useMemo } from 'react';
import { useForumData } from '@/hooks'; import { useForum } from '@/hooks';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
@ -35,54 +35,48 @@ interface CommentFeedItem extends FeedItemBase {
type FeedItem = PostFeedItem | CommentFeedItem; type FeedItem = PostFeedItem | CommentFeedItem;
const ActivityFeed: React.FC = () => { const ActivityFeed: React.FC = () => {
// ✅ Use reactive hooks for data const { content, network } = useForum();
const forumData = useForumData();
const { const { posts, comments, cells, commentsByPost } = content;
postsWithVoteStatus, const { isConnected } = network;
commentsWithVoteStatus,
cellsWithStats,
isInitialLoading,
} = forumData;
// ✅ Use pre-computed data with vote scores
const combinedFeed: FeedItem[] = [ const combinedFeed: FeedItem[] = useMemo(() => {
...postsWithVoteStatus.map( return [
(post): PostFeedItem => ({ ...posts.map(
id: post.id, (post): PostFeedItem => ({
type: 'post', ...post,
timestamp: post.timestamp, type: 'post',
ownerAddress: post.author, ownerAddress: post.authorAddress,
title: post.title, cellId: post.cellId,
cellId: post.cellId, postId: post.id,
postId: post.id, title: post.title,
commentCount: forumData.commentsByPost[post.id]?.length || 0, commentCount: commentsByPost[post.id]?.length || 0,
voteCount: post.voteScore, voteCount: post.upvotes.length - post.downvotes.length,
}) })
), ),
...commentsWithVoteStatus ...comments
.map((comment): CommentFeedItem | null => { .map((comment): CommentFeedItem | null => {
const parentPost = postsWithVoteStatus.find( const parentPost = posts.find(p => p.id === comment.postId);
p => p.id === comment.postId if (!parentPost) return null;
); return {
if (!parentPost) return null; id: comment.id,
return { type: 'comment',
id: comment.id, timestamp: comment.timestamp,
type: 'comment', ownerAddress: comment.author,
timestamp: comment.timestamp, content: comment.content,
ownerAddress: comment.author, postId: comment.postId,
content: comment.content, cellId: parentPost.cellId,
postId: comment.postId, voteCount: comment.upvotes.length - comment.downvotes.length,
cellId: parentPost.cellId, };
voteCount: comment.voteScore, })
}; .filter((item): item is CommentFeedItem => item !== null),
}) ].sort((a, b) => b.timestamp - a.timestamp);
.filter((item): item is CommentFeedItem => item !== null), }, [posts, comments, commentsByPost]);
].sort((a, b) => b.timestamp - a.timestamp);
const renderFeedItem = (item: FeedItem) => { const renderFeedItem = (item: FeedItem) => {
const cell = item.cellId const cell = item.cellId
? cellsWithStats.find(c => c.id === item.cellId) ? cells.find(c => c.id === item.cellId)
: undefined; : undefined;
const timeAgo = formatDistanceToNow(new Date(item.timestamp), { const timeAgo = formatDistanceToNow(new Date(item.timestamp), {
addSuffix: true, addSuffix: true,
@ -150,7 +144,7 @@ const ActivityFeed: React.FC = () => {
); );
}; };
if (isInitialLoading) { if (!isConnected) {
return ( return (
<div className="space-y-3"> <div className="space-y-3">
{[...Array(5)].map((_, i) => ( {[...Array(5)].map((_, i) => (

View File

@ -1,6 +1,6 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useContent, usePermissions } from '@/hooks'; import { useContent, usePermissions } from '@/hooks';
import { import {
Layout, Layout,
MessageSquare, MessageSquare,
@ -226,9 +226,7 @@ const CellList = () => {
title="Refresh data" title="Refresh data"
className="px-3 border-cyber-muted/30 text-cyber-neutral hover:bg-cyber-muted/30" className="px-3 border-cyber-muted/30 text-cyber-neutral hover:bg-cyber-muted/30"
> >
<RefreshCw <RefreshCw className="w-4 h-4" />
className="w-4 h-4"
/>
</Button> </Button>
{canCreateCell && <CreateCellDialog />} {canCreateCell && <CreateCellDialog />}

View File

@ -61,10 +61,14 @@ const CommentCard: React.FC<CommentCardProps> = ({
// Use library pending API // Use library pending API
const commentVotePending = content.pending.isPending(comment.id); const commentVotePending = content.pending.isPending(comment.id);
const score = comment.voteScore ?? 0; const score = comment.upvotes.length - comment.downvotes.length;
const isModerated = Boolean(comment.moderated); const isModerated = Boolean(comment.moderated);
const userDownvoted = Boolean(comment.downvotes.some(v => v.author === currentUser?.address)); const userDownvoted = Boolean(
const userUpvoted = Boolean(comment.upvotes.some(v => v.author === currentUser?.address)); comment.downvotes.some(v => v.author === currentUser?.address)
);
const userUpvoted = Boolean(
comment.upvotes.some(v => v.author === currentUser?.address)
);
const isOwnComment = currentUser?.address === comment.author; const isOwnComment = currentUser?.address === comment.author;
const handleVoteComment = async (isUpvote: boolean) => { const handleVoteComment = async (isUpvote: boolean) => {
@ -86,7 +90,9 @@ const CommentCard: React.FC<CommentCardProps> = ({
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<button <button
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${ className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${
userUpvoted ? 'text-cyber-accent' : 'text-cyber-neutral hover:text-cyber-accent' userUpvoted
? 'text-cyber-accent'
: 'text-cyber-neutral hover:text-cyber-accent'
}`} }`}
onClick={() => handleVoteComment(true)} onClick={() => handleVoteComment(true)}
disabled={!permissions.canVote} disabled={!permissions.canVote}
@ -99,7 +105,9 @@ const CommentCard: React.FC<CommentCardProps> = ({
<span className="text-sm font-bold">{score}</span> <span className="text-sm font-bold">{score}</span>
<button <button
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${ className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${
userDownvoted ? 'text-cyber-accent' : 'text-cyber-neutral hover:text-cyber-accent' userDownvoted
? 'text-cyber-accent'
: 'text-cyber-neutral hover:text-cyber-accent'
}`} }`}
onClick={() => handleVoteComment(false)} onClick={() => handleVoteComment(false)}
disabled={!permissions.canVote} disabled={!permissions.canVote}

View File

@ -79,7 +79,8 @@ export function CreateCellDialog({
if (!canCreateCell) { if (!canCreateCell) {
toast({ toast({
title: 'Permission Denied', title: 'Permission Denied',
description: 'Only verified ENS or Logos Ordinal owners can create cells.', description:
'Only verified ENS or Logos Ordinal owners can create cells.',
variant: 'destructive', variant: 'destructive',
}); });
return; return;

View File

@ -4,16 +4,15 @@ import { TrendingUp, Users, Eye, CheckCircle } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { useAuth, useContent } from '@/hooks'; import { useAuth, useContent } from '@/hooks';
import { EVerificationStatus } from '@opchan/core'; import { EVerificationStatus } from '@opchan/core';
import { CypherImage } from '@/components/ui/CypherImage'; import { CypherImage } from '@/components/ui/CypherImage';
const FeedSidebar: React.FC = () => { const FeedSidebar: React.FC = () => {
const {cells, posts, comments, cellsWithStats, userVerificationStatus} = useContent(); const { cells, posts, comments, cellsWithStats, userVerificationStatus } =
useContent();
const { currentUser, verificationStatus } = useAuth(); const { currentUser, verificationStatus } = useAuth();
const stats = { const stats = {
totalCells: cells.length, totalCells: cells.length,
totalPosts: posts.length, totalPosts: posts.length,
@ -61,7 +60,9 @@ const FeedSidebar: React.FC = () => {
<Users className="w-5 h-5 text-cyber-accent" /> <Users className="w-5 h-5 text-cyber-accent" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="font-medium text-sm">{currentUser?.displayName}</div> <div className="font-medium text-sm">
{currentUser?.displayName}
</div>
<Badge <Badge
variant="secondary" variant="secondary"
className={`${verificationBadge.color} text-white text-xs`} className={`${verificationBadge.color} text-white text-xs`}

View File

@ -49,9 +49,9 @@ import { WakuHealthDot } from '@/components/ui/waku-health-indicator';
const Header = () => { const Header = () => {
const { currentUser, delegationInfo } = useAuth(); const { currentUser, delegationInfo } = useAuth();
const {statusMessage} = useNetwork(); const { statusMessage } = useNetwork();
const location = useLocation() const location = useLocation();
const { toast } = useToast(); const { toast } = useToast();
const { content } = useForum(); const { content } = useForum();
@ -65,7 +65,10 @@ const Header = () => {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
// Use centralized UI state instead of direct LocalDatabase access // Use centralized UI state instead of direct LocalDatabase access
const [hasShownWizard, setHasShownWizard] = useUIState('hasShownWalletWizard', false); const [hasShownWizard, setHasShownWizard] = useUIState(
'hasShownWalletWizard',
false
);
// Auto-open wizard when wallet connects for the first time // Auto-open wizard when wallet connects for the first time
React.useEffect(() => { React.useEffect(() => {
@ -109,23 +112,26 @@ const Header = () => {
} }
}; };
useEffect(() => { useEffect(() => {
console.log('currentUser', currentUser) console.log('currentUser', currentUser);
}, [currentUser]) }, [currentUser]);
const getStatusIcon = () => { const getStatusIcon = () => {
if (!isConnected) return <CircleSlash className="w-4 h-4" />; if (!isConnected) return <CircleSlash className="w-4 h-4" />;
if ( if (
currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED && currentUser?.verificationStatus ===
EVerificationStatus.ENS_ORDINAL_VERIFIED &&
delegationInfo?.isValid delegationInfo?.isValid
) { ) {
return <CheckCircle className="w-4 h-4" />; return <CheckCircle className="w-4 h-4" />;
} else if (currentUser?.verificationStatus === EVerificationStatus.WALLET_CONNECTED) { } else if (
currentUser?.verificationStatus === EVerificationStatus.WALLET_CONNECTED
) {
return <AlertTriangle className="w-4 h-4" />; return <AlertTriangle className="w-4 h-4" />;
} else if ( } else if (
currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED currentUser?.verificationStatus ===
EVerificationStatus.ENS_ORDINAL_VERIFIED
) { ) {
return <Key className="w-4 h-4" />; return <Key className="w-4 h-4" />;
} else { } else {
@ -197,11 +203,13 @@ const Header = () => {
> >
{getStatusIcon()} {getStatusIcon()}
<span className="ml-1"> <span className="ml-1">
{currentUser?.verificationStatus === EVerificationStatus.WALLET_UNCONNECTED {currentUser?.verificationStatus ===
EVerificationStatus.WALLET_UNCONNECTED
? 'CONNECT' ? 'CONNECT'
: delegationInfo?.isValid : delegationInfo?.isValid
? 'READY' ? 'READY'
: currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED : currentUser?.verificationStatus ===
EVerificationStatus.ENS_ORDINAL_VERIFIED
? 'EXPIRED' ? 'EXPIRED'
: 'DELEGATE'} : 'DELEGATE'}
</span> </span>
@ -215,7 +223,9 @@ const Header = () => {
size="sm" size="sm"
className="flex items-center space-x-2 text-white hover:bg-cyber-muted/30" className="flex items-center space-x-2 text-white hover:bg-cyber-muted/30"
> >
<div className="text-sm font-mono">{currentUser?.displayName}</div> <div className="text-sm font-mono">
{currentUser?.displayName}
</div>
<Settings className="w-4 h-4" /> <Settings className="w-4 h-4" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>

View File

@ -2,59 +2,52 @@ import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ArrowUp, ArrowDown, MessageSquare } from 'lucide-react'; import { ArrowUp, ArrowDown, MessageSquare } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import type { Post, PostMessage } from '@opchan/core'; import type { Post } from '@opchan/core';
import { RelevanceIndicator } from '@/components/ui/relevance-indicator'; import { RelevanceIndicator } from '@/components/ui/relevance-indicator';
import { AuthorDisplay } from '@/components/ui/author-display'; import { AuthorDisplay } from '@/components/ui/author-display';
import { BookmarkButton } from '@/components/ui/bookmark-button'; import { BookmarkButton } from '@/components/ui/bookmark-button';
import { LinkRenderer } from '@/components/ui/link-renderer'; import { LinkRenderer } from '@/components/ui/link-renderer';
import { useContent, usePermissions } from '@/hooks'; import { useAuth, useContent, usePermissions } from '@/hooks';
import { ShareButton } from '@/components/ui/ShareButton'; import { ShareButton } from '@/components/ui/ShareButton';
interface PostCardProps { interface PostCardProps {
post: Post | PostMessage; post: Post;
commentCount?: number;
} }
const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => { const PostCard: React.FC<PostCardProps> = ({ post }) => {
const content = useContent(); const {
bookmarks,
pending,
vote,
togglePostBookmark,
cells,
commentsByPost,
} = useContent();
const permissions = usePermissions(); const permissions = usePermissions();
const { currentUser } = useAuth();
// Get cell data from content const cellName = cells.find(c => c.id === post.cellId)?.name || 'unknown';
const cell = content.cells.find((c) => c.id === post.cellId); const commentCount = commentsByPost[post.id]?.length || 0;
const cellName = cell?.name || 'unknown';
// Use pre-computed vote data or safely compute from arrays when available const isPending = pending.isPending(post.id);
const computedVoteScore =
'voteScore' in post && typeof (post as Post).voteScore === 'number'
? (post as Post).voteScore
: undefined;
const upvoteCount =
'upvotes' in post && Array.isArray((post as Post).upvotes)
? (post as Post).upvotes.length
: 0;
const downvoteCount =
'downvotes' in post && Array.isArray((post as Post).downvotes)
? (post as Post).downvotes.length
: 0;
const score = computedVoteScore ?? upvoteCount - downvoteCount;
// Use library pending API const isBookmarked = bookmarks.some(
const isPending = content.pending.isPending(post.id); b => b.targetId === post.id && b.type === 'post'
);
// Get user vote status from post data
const userUpvoted =
(post as unknown as { userUpvoted?: boolean }).userUpvoted || false;
const userDownvoted =
(post as unknown as { userDownvoted?: boolean }).userDownvoted || false;
// Check if bookmarked
const isBookmarked = content.bookmarks.some((b) => b.targetId === post.id && b.type === 'post');
const [bookmarkLoading, setBookmarkLoading] = React.useState(false); const [bookmarkLoading, setBookmarkLoading] = React.useState(false);
// Remove duplicate vote status logic const score = post.upvotes.length - post.downvotes.length;
const userUpvoted = Boolean(
post.upvotes.some(v => v.author === currentUser?.address)
);
const userDownvoted = Boolean(
post.downvotes.some(v => v.author === currentUser?.address)
);
// ✅ Content truncation (simple presentation logic is OK) const contentText =
const contentText = typeof post.content === 'string' ? post.content : String(post.content ?? ''); typeof post.content === 'string'
? post.content
: String(post.content ?? '');
const contentPreview = const contentPreview =
contentText.length > 200 contentText.length > 200
? contentText.substring(0, 200) + '...' ? contentText.substring(0, 200) + '...'
@ -62,7 +55,7 @@ const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => {
const handleVote = async (e: React.MouseEvent, isUpvote: boolean) => { const handleVote = async (e: React.MouseEvent, isUpvote: boolean) => {
e.preventDefault(); e.preventDefault();
await content.vote({ targetId: post.id, isUpvote }); await vote({ targetId: post.id, isUpvote });
}; };
const handleBookmark = async (e?: React.MouseEvent) => { const handleBookmark = async (e?: React.MouseEvent) => {
@ -72,7 +65,7 @@ const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => {
} }
setBookmarkLoading(true); setBookmarkLoading(true);
try { try {
await content.togglePostBookmark(post, post.cellId); await togglePostBookmark(post, post.cellId);
} finally { } finally {
setBookmarkLoading(false); setBookmarkLoading(false);
} }
@ -97,13 +90,7 @@ const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => {
</button> </button>
<span <span
className={`text-sm font-medium px-1 ${ className={`text-sm font-medium px-1`}
score > 0
? 'text-cyber-accent'
: score < 0
? 'text-blue-400'
: 'text-cyber-neutral'
}`}
> >
{score} {score}
</span> </span>
@ -130,9 +117,17 @@ const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => {
<div className="block hover:opacity-80"> <div className="block hover:opacity-80">
{/* Post metadata */} {/* Post metadata */}
<div className="flex items-center text-xs text-cyber-neutral mb-2 space-x-2"> <div className="flex items-center text-xs text-cyber-neutral mb-2 space-x-2">
<span className="font-medium text-cyber-accent"> <Link
r/{cellName} to={cellName ? `/cell/${post.cellId}` : "#"}
</span> className="font-medium text-cyber-accent hover:underline focus:underline"
tabIndex={0}
onClick={e => {
if (!cellName) e.preventDefault();
}}
title={cellName ? `Go to /${cellName}` : undefined}
>
r/{cellName || 'unknown'}
</Link>
<span></span> <span></span>
<span>Posted by u/</span> <span>Posted by u/</span>
<AuthorDisplay <AuthorDisplay
@ -146,18 +141,23 @@ const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => {
addSuffix: true, addSuffix: true,
})} })}
</span> </span>
{('relevanceScore' in post) && typeof (post as Post).relevanceScore === 'number' && ( {'relevanceScore' in post &&
<> typeof (post as Post).relevanceScore === 'number' && (
<span></span> <>
<RelevanceIndicator <span></span>
score={(post as Post).relevanceScore as number} <RelevanceIndicator
details={('relevanceDetails' in post ? (post as Post).relevanceDetails : undefined)} score={(post as Post).relevanceScore as number}
type="post" details={
className="text-xs" 'relevanceDetails' in post
showTooltip={true} ? (post as Post).relevanceDetails
/> : undefined
</> }
)} type="post"
className="text-xs"
showTooltip={true}
/>
</>
)}
</div> </div>
{/* Post title and content - clickable to navigate to post */} {/* Post title and content - clickable to navigate to post */}

View File

@ -17,7 +17,7 @@ import { AuthorDisplay } from './ui/author-display';
import { BookmarkButton } from './ui/bookmark-button'; import { BookmarkButton } from './ui/bookmark-button';
import { MarkdownRenderer } from './ui/markdown-renderer'; import { MarkdownRenderer } from './ui/markdown-renderer';
import CommentCard from './CommentCard'; import CommentCard from './CommentCard';
import { useContent, usePermissions } from '@/hooks'; import { useAuth, useContent, usePermissions } from '@/hooks';
import type { Cell as ForumCell } from '@opchan/core'; import type { Cell as ForumCell } from '@opchan/core';
import { ShareButton } from './ui/ShareButton'; import { ShareButton } from './ui/ShareButton';
@ -28,17 +28,19 @@ const PostDetail = () => {
// Use aggregated forum API // Use aggregated forum API
const content = useContent(); const content = useContent();
const permissions = usePermissions(); const permissions = usePermissions();
const { currentUser } = useAuth();
// Get post and comments using focused hooks // Get post and comments using focused hooks
const post = content.posts.find((p) => p.id === postId); const post = content.posts.find(p => p.id === postId);
const visibleComments = postId ? content.commentsByPost[postId] ?? [] : []; const visibleComments = postId ? (content.commentsByPost[postId] ?? []) : [];
// Use library pending API // Use library pending API
const postPending = content.pending.isPending(post?.id); const postPending = content.pending.isPending(post?.id);
const postVotePending = content.pending.isPending(post?.id); const postVotePending = content.pending.isPending(post?.id);
// Check if bookmarked // Check if bookmarked
const isBookmarked = content.bookmarks.some((b) => b.targetId === post?.id && b.type === 'post'); const isBookmarked = content.bookmarks.some(
b => b.targetId === post?.id && b.type === 'post'
);
const [bookmarkLoading, setBookmarkLoading] = React.useState(false); const [bookmarkLoading, setBookmarkLoading] = React.useState(false);
const [newComment, setNewComment] = useState(''); const [newComment, setNewComment] = useState('');
@ -115,11 +117,13 @@ const PostDetail = () => {
} }
}; };
// Get vote status from post data (enhanced posts only) const score = post.upvotes.length - post.downvotes.length;
const enhanced = post as unknown as { userUpvoted?: boolean; userDownvoted?: boolean; voteScore?: number }; const isPostUpvoted = Boolean(
const isPostUpvoted = Boolean(enhanced.userUpvoted); post.upvotes.some(v => v.author === currentUser?.address)
const isPostDownvoted = Boolean(enhanced.userDownvoted); );
const score = typeof enhanced.voteScore === 'number' ? enhanced.voteScore : 0; const isPostDownvoted = Boolean(
post.downvotes.some(v => v.author === currentUser?.address)
);
const handleModerateComment = async (commentId: string) => { const handleModerateComment = async (commentId: string) => {
const reason = const reason =
@ -160,7 +164,9 @@ const PostDetail = () => {
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<button <button
className={`p-1 rounded-sm hover:bg-muted/50 ${ className={`p-1 rounded-sm hover:bg-muted/50 ${
isPostUpvoted ? 'text-primary' : '' isPostUpvoted
? 'text-cyber-accent'
: 'text-cyber-neutral hover:text-cyber-accent'
}`} }`}
onClick={() => handleVotePost(true)} onClick={() => handleVotePost(true)}
disabled={!permissions.canVote} disabled={!permissions.canVote}
@ -173,7 +179,9 @@ const PostDetail = () => {
<span className="text-sm font-bold">{score}</span> <span className="text-sm font-bold">{score}</span>
<button <button
className={`p-1 rounded-sm hover:bg-muted/50 ${ className={`p-1 rounded-sm hover:bg-muted/50 ${
isPostDownvoted ? 'text-primary' : '' isPostDownvoted
? 'text-cyber-accent'
: 'text-cyber-neutral hover:text-cyber-accent'
}`} }`}
onClick={() => handleVotePost(false)} onClick={() => handleVotePost(false)}
disabled={!permissions.canVote} disabled={!permissions.canVote}
@ -194,9 +202,17 @@ const PostDetail = () => {
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-2"> <div className="flex items-center gap-2 text-sm text-muted-foreground mb-2">
<span className="font-medium text-primary"> <Link
to={cell?.id ? `/cell/${cell.id}` : "#"}
className="font-medium text-primary hover:underline focus:underline"
tabIndex={0}
onClick={e => {
if (!cell?.id) e.preventDefault();
}}
title={cell?.name ? `Go to /${cell.name}` : undefined}
>
r/{cell?.name || 'unknown'} r/{cell?.name || 'unknown'}
</span> </Link>
<span></span> <span></span>
<span>Posted by u/</span> <span>Posted by u/</span>
<AuthorDisplay <AuthorDisplay
@ -279,9 +295,7 @@ const PostDetail = () => {
{!permissions.canComment && ( {!permissions.canComment && (
<div className="mb-6 p-4 border border-cyber-muted rounded-sm bg-cyber-muted/20 text-center"> <div className="mb-6 p-4 border border-cyber-muted rounded-sm bg-cyber-muted/20 text-center">
<p className="text-sm mb-3"> <p className="text-sm mb-3">Connect your wallet to comment</p>
Connect your wallet to comment
</p>
<Button asChild size="sm"> <Button asChild size="sm">
<Link to="/">Connect Wallet</Link> <Link to="/">Connect Wallet</Link>
</Button> </Button>
@ -306,7 +320,7 @@ const PostDetail = () => {
</p> </p>
</div> </div>
) : ( ) : (
visibleComments.map((comment) => ( visibleComments.map(comment => (
<CommentCard <CommentCard
key={comment.id} key={comment.id}
comment={comment} comment={comment}

View File

@ -1,7 +1,11 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Link, useParams } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
import { usePermissions, useAuth, useContent } from '@/hooks'; import { usePermissions, useAuth, useContent } from '@/hooks';
import type { Post as ForumPost, Cell as ForumCell, VoteMessage } from '@opchan/core'; import type {
Post as ForumPost,
Cell as ForumCell,
VoteMessage,
} from '@opchan/core';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
@ -31,8 +35,8 @@ import {
const PostList = () => { const PostList = () => {
const { cellId } = useParams<{ cellId: string }>(); const { cellId } = useParams<{ cellId: string }>();
// ✅ Use reactive hooks for data and actions const { createPost, vote, moderate, refresh, commentsByPost, cells, posts } =
const { createPost, vote, moderate, refresh, commentsByPost, cells, posts } = useContent(); useContent();
const cell = cells.find((c: ForumCell) => c.id === cellId); const cell = cells.find((c: ForumCell) => c.id === cellId);
const isCreatingPost = false; const isCreatingPost = false;
const isVoting = false; const isVoting = false;
@ -101,7 +105,11 @@ const PostList = () => {
if (!newPostContent.trim()) return; if (!newPostContent.trim()) return;
// ✅ All validation handled in hook // ✅ All validation handled in hook
const post = await createPost({ cellId, title: newPostTitle, content: newPostContent }); const post = await createPost({
cellId,
title: newPostTitle,
content: newPostContent,
});
if (post) { if (post) {
setNewPostTitle(''); setNewPostTitle('');
setNewPostContent(''); setNewPostContent('');
@ -129,8 +137,12 @@ const PostList = () => {
if (!currentUser) return null; if (!currentUser) return null;
const p = posts.find((p: ForumPost) => p.id === postId); const p = posts.find((p: ForumPost) => p.id === postId);
if (!p) return null; if (!p) return null;
const up = p.upvotes.some((v: VoteMessage) => v.author === currentUser.address); const up = p.upvotes.some(
const down = p.downvotes.some((v: VoteMessage) => v.author === currentUser.address); (v: VoteMessage) => v.author === currentUser.address
);
const down = p.downvotes.some(
(v: VoteMessage) => v.author === currentUser.address
);
return up ? 'upvote' : down ? 'downvote' : null; return up ? 'upvote' : down ? 'downvote' : null;
}; };
@ -248,9 +260,7 @@ const PostList = () => {
{!canPost && !currentUser && ( {!canPost && !currentUser && (
<div className="section-spacing content-card-sm text-center"> <div className="section-spacing content-card-sm text-center">
<p className="text-sm mb-3"> <p className="text-sm mb-3">Connect your wallet to post</p>
Connect your wallet to post
</p>
<Button asChild size="sm"> <Button asChild size="sm">
<Link to="/">Connect Wallet</Link> <Link to="/">Connect Wallet</Link>
</Button> </Button>
@ -277,9 +287,7 @@ const PostList = () => {
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${getPostVoteType(post.id) === 'upvote' ? 'text-cyber-accent' : ''}`} className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${getPostVoteType(post.id) === 'upvote' ? 'text-cyber-accent' : ''}`}
onClick={() => handleVotePost(post.id, true)} onClick={() => handleVotePost(post.id, true)}
disabled={!canVote || isVoting} disabled={!canVote || isVoting}
title={ title={canVote ? 'Upvote' : 'Connect your wallet to vote'}
canVote ? 'Upvote' : 'Connect your wallet to vote'
}
> >
<ArrowUp className="w-4 h-4" /> <ArrowUp className="w-4 h-4" />
</button> </button>
@ -290,9 +298,7 @@ const PostList = () => {
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${getPostVoteType(post.id) === 'downvote' ? 'text-cyber-accent' : ''}`} className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${getPostVoteType(post.id) === 'downvote' ? 'text-cyber-accent' : ''}`}
onClick={() => handleVotePost(post.id, false)} onClick={() => handleVotePost(post.id, false)}
disabled={!canVote || isVoting} disabled={!canVote || isVoting}
title={ title={canVote ? 'Downvote' : 'Connect your wallet to vote'}
canVote ? 'Downvote' : 'Connect your wallet to vote'
}
> >
<ArrowDown className="w-4 h-4" /> <ArrowDown className="w-4 h-4" />
</button> </button>

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { cn } from '../../utils' import { cn } from '../../utils';
type CypherImageProps = { type CypherImageProps = {
src?: string; src?: string;

View File

@ -1,6 +1,6 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Share2 } from 'lucide-react'; import { Share2 } from 'lucide-react';
import { cn } from '../../utils' import { cn } from '../../utils';
import { useToast } from '../ui/use-toast'; import { useToast } from '../ui/use-toast';
interface ShareButtonProps { interface ShareButtonProps {

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import * as AccordionPrimitive from '@radix-ui/react-accordion'; import * as AccordionPrimitive from '@radix-ui/react-accordion';
import { ChevronDown } from 'lucide-react'; import { ChevronDown } from 'lucide-react';
import { cn } from '../../utils' import { cn } from '../../utils';
const Accordion = AccordionPrimitive.Root; const Accordion = AccordionPrimitive.Root;

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
import { cn } from '../../utils' import { cn } from '../../utils';
import { buttonVariants } from '@/components/ui/button-variants'; import { buttonVariants } from '@/components/ui/button-variants';
const AlertDialog = AlertDialogPrimitive.Root; const AlertDialog = AlertDialogPrimitive.Root;

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../../utils' import { cn } from '../../utils';
const alertVariants = cva( const alertVariants = cva(
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',

View File

@ -14,11 +14,12 @@ export function AuthorDisplay({
className = '', className = '',
showBadge = true, showBadge = true,
}: AuthorDisplayProps) { }: AuthorDisplayProps) {
const { ensName, ordinalDetails, callSign, displayName } = useUserDisplay(address); const { ensName, ordinalDetails, callSign, displayName } =
useUserDisplay(address);
useEffect(()=> { useEffect(() => {
console.log({ensName, ordinalDetails, callSign, displayName, address}) console.log({ ensName, ordinalDetails, callSign, displayName, address });
}, [address, ensName, ordinalDetails, callSign, displayName]) }, [address, ensName, ordinalDetails, callSign, displayName]);
// Only show a badge if the author has ENS, Ordinal, or Call Sign // Only show a badge if the author has ENS, Ordinal, or Call Sign
const shouldShowBadge = showBadge && (ensName || ordinalDetails || callSign); const shouldShowBadge = showBadge && (ensName || ordinalDetails || callSign);

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import * as AvatarPrimitive from '@radix-ui/react-avatar'; import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { cn } from '../../utils' import { cn } from '../../utils';
const Avatar = React.forwardRef< const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>, React.ElementRef<typeof AvatarPrimitive.Root>,

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../../utils' import { cn } from '../../utils';
const badgeVariants = cva( const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',

View File

@ -1,6 +1,6 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Bookmark, BookmarkCheck } from 'lucide-react'; import { Bookmark, BookmarkCheck } from 'lucide-react';
import { cn } from '../../utils' import { cn } from '../../utils';
interface BookmarkButtonProps { interface BookmarkButtonProps {
isBookmarked: boolean; isBookmarked: boolean;

View File

@ -10,7 +10,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { Bookmark, BookmarkType } from '@opchan/core'; import { Bookmark, BookmarkType } from '@opchan/core';
import { useUserDisplay } from '@opchan/react'; import { useUserDisplay } from '@opchan/react';
import { cn } from '../../utils' import { cn } from '../../utils';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import { Slot } from '@radix-ui/react-slot'; import { Slot } from '@radix-ui/react-slot';
import { ChevronRight, MoreHorizontal } from 'lucide-react'; import { ChevronRight, MoreHorizontal } from 'lucide-react';
import { cn } from '../../utils' import { cn } from '../../utils';
const Breadcrumb = React.forwardRef< const Breadcrumb = React.forwardRef<
HTMLElement, HTMLElement,

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import { Slot } from '@radix-ui/react-slot'; import { Slot } from '@radix-ui/react-slot';
import { type VariantProps } from 'class-variance-authority'; import { type VariantProps } from 'class-variance-authority';
import { cn } from '../../utils' import { cn } from '../../utils';
import { buttonVariants } from './button-variants'; import { buttonVariants } from './button-variants';
export interface ButtonProps export interface ButtonProps

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import { ChevronLeft, ChevronRight } from 'lucide-react'; import { ChevronLeft, ChevronRight } from 'lucide-react';
import { DayPicker } from 'react-day-picker'; import { DayPicker } from 'react-day-picker';
import { cn } from '../../utils' import { cn } from '../../utils';
import { buttonVariants } from '@/components/ui/button-variants'; import { buttonVariants } from '@/components/ui/button-variants';
export type CalendarProps = React.ComponentProps<typeof DayPicker>; export type CalendarProps = React.ComponentProps<typeof DayPicker>;

View File

@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { cn } from '../../utils' import { cn } from '../../utils';
const Card = React.forwardRef< const Card = React.forwardRef<
HTMLDivElement, HTMLDivElement,

View File

@ -4,7 +4,7 @@ import useEmblaCarousel, {
} from 'embla-carousel-react'; } from 'embla-carousel-react';
import { ArrowLeft, ArrowRight } from 'lucide-react'; import { ArrowLeft, ArrowRight } from 'lucide-react';
import { cn } from '../../utils' import { cn } from '../../utils';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
type CarouselApi = UseEmblaCarouselType[1]; type CarouselApi = UseEmblaCarouselType[1];

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import * as RechartsPrimitive from 'recharts'; import * as RechartsPrimitive from 'recharts';
import { cn } from '../../utils' import { cn } from '../../utils';
// Format: { THEME_NAME: CSS_SELECTOR } // Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: '', dark: '.dark' } as const; const THEMES = { light: '', dark: '.dark' } as const;

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { Check } from 'lucide-react'; import { Check } from 'lucide-react';
import { cn } from '../../utils' import { cn } from '../../utils';
const Checkbox = React.forwardRef< const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>, React.ElementRef<typeof CheckboxPrimitive.Root>,

View File

@ -3,7 +3,7 @@ import { type DialogProps } from '@radix-ui/react-dialog';
import { Command as CommandPrimitive } from 'cmdk'; import { Command as CommandPrimitive } from 'cmdk';
import { Search } from 'lucide-react'; import { Search } from 'lucide-react';
import { cn } from '../../utils' import { cn } from '../../utils';
import { Dialog, DialogContent } from '@/components/ui/dialog'; import { Dialog, DialogContent } from '@/components/ui/dialog';
const Command = React.forwardRef< const Command = React.forwardRef<

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu'; import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
import { Check, ChevronRight, Circle } from 'lucide-react'; import { Check, ChevronRight, Circle } from 'lucide-react';
import { cn } from '../../utils' import { cn } from '../../utils';
const ContextMenu = ContextMenuPrimitive.Root; const ContextMenu = ContextMenuPrimitive.Root;

View File

@ -194,7 +194,9 @@ export function DelegationStep({
{/* User Address */} {/* User Address */}
{currentUser && ( {currentUser && (
<div className="text-xs text-neutral-400"> <div className="text-xs text-neutral-400">
<div className="font-mono break-all">{currentUser.displayName}</div> <div className="font-mono break-all">
{currentUser.displayName}
</div>
</div> </div>
)} )}

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog'; import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import { cn } from '../../utils' import { cn } from '../../utils';
const Dialog = DialogPrimitive.Root; const Dialog = DialogPrimitive.Root;

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { Drawer as DrawerPrimitive } from 'vaul'; import { Drawer as DrawerPrimitive } from 'vaul';
import { cn } from '../../utils' import { cn } from '../../utils';
const Drawer = ({ const Drawer = ({
shouldScaleBackground = true, shouldScaleBackground = true,

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { Check, ChevronRight, Circle } from 'lucide-react'; import { Check, ChevronRight, Circle } from 'lucide-react';
import { cn } from '../../utils' import { cn } from '../../utils';
const DropdownMenu = DropdownMenuPrimitive.Root; const DropdownMenu = DropdownMenuPrimitive.Root;

View File

@ -10,7 +10,7 @@ import {
useFormContext, useFormContext,
} from 'react-hook-form'; } from 'react-hook-form';
import { cn } from '../../utils' import { cn } from '../../utils';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
const Form = FormProvider; const Form = FormProvider;

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import * as HoverCardPrimitive from '@radix-ui/react-hover-card'; import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
import { cn } from '../../utils' import { cn } from '../../utils';
const HoverCard = HoverCardPrimitive.Root; const HoverCard = HoverCardPrimitive.Root;

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import { OTPInput, OTPInputContext } from 'input-otp'; import { OTPInput, OTPInputContext } from 'input-otp';
import { Dot } from 'lucide-react'; import { Dot } from 'lucide-react';
import { cn } from '../../utils' import { cn } from '../../utils';
const InputOTP = React.forwardRef< const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>, React.ElementRef<typeof OTPInput>,

View File

@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { cn } from '../../utils' import { cn } from '../../utils';
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>( const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
({ className, type, ...props }, ref) => { ({ className, type, ...props }, ref) => {

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label'; import * as LabelPrimitive from '@radix-ui/react-label';
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../../utils' import { cn } from '../../utils';
const labelVariants = cva( const labelVariants = cva(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import * as MenubarPrimitive from '@radix-ui/react-menubar'; import * as MenubarPrimitive from '@radix-ui/react-menubar';
import { Check, ChevronRight, Circle } from 'lucide-react'; import { Check, ChevronRight, Circle } from 'lucide-react';
import { cn } from '../../utils' import { cn } from '../../utils';
const MenubarMenu = MenubarPrimitive.Menu; const MenubarMenu = MenubarPrimitive.Menu;

View File

@ -8,8 +8,14 @@ export function ModerationToggle() {
const { canModerate } = usePermissions(); const { canModerate } = usePermissions();
const { cellsWithStats } = useContent(); const { cellsWithStats } = useContent();
const [showModerated, setShowModerated] = useUIState<boolean>('showModerated', false); const [showModerated, setShowModerated] = useUIState<boolean>(
const toggleShowModerated = React.useCallback((value: boolean) => setShowModerated(value), [setShowModerated]); 'showModerated',
false
);
const toggleShowModerated = React.useCallback(
(value: boolean) => setShowModerated(value),
[setShowModerated]
);
// Check if user is admin of any cell // Check if user is admin of any cell
const isAdminOfAnyCell = cellsWithStats.some(cell => canModerate(cell.id)); const isAdminOfAnyCell = cellsWithStats.some(cell => canModerate(cell.id));

View File

@ -3,7 +3,7 @@ import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';
import { cva } from 'class-variance-authority'; import { cva } from 'class-variance-authority';
import { ChevronDown } from 'lucide-react'; import { ChevronDown } from 'lucide-react';
import { cn } from '../../utils' import { cn } from '../../utils';
const NavigationMenu = React.forwardRef< const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>, React.ElementRef<typeof NavigationMenuPrimitive.Root>,

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react'; import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
import { cn } from '../../utils' import { cn } from '../../utils';
import { ButtonProps } from '@/components/ui/button'; import { ButtonProps } from '@/components/ui/button';
import { buttonVariants } from '@/components/ui/button-variants'; import { buttonVariants } from '@/components/ui/button-variants';

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import * as PopoverPrimitive from '@radix-ui/react-popover'; import * as PopoverPrimitive from '@radix-ui/react-popover';
import { cn } from '../../utils' import { cn } from '../../utils';
const Popover = PopoverPrimitive.Root; const Popover = PopoverPrimitive.Root;

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import * as ProgressPrimitive from '@radix-ui/react-progress'; import * as ProgressPrimitive from '@radix-ui/react-progress';
import { cn } from '../../utils' import { cn } from '../../utils';
const Progress = React.forwardRef< const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>, React.ElementRef<typeof ProgressPrimitive.Root>,

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'; import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
import { Circle } from 'lucide-react'; import { Circle } from 'lucide-react';
import { cn } from '../../utils' import { cn } from '../../utils';
const RadioGroup = React.forwardRef< const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>, React.ElementRef<typeof RadioGroupPrimitive.Root>,

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { Resizable } from 're-resizable'; import { Resizable } from 're-resizable';
import { cn } from '../../utils' import { cn } from '../../utils';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
type ResizableTextareaProps = type ResizableTextareaProps =

View File

@ -1,7 +1,7 @@
import { GripVertical } from 'lucide-react'; import { GripVertical } from 'lucide-react';
import * as ResizablePrimitive from 'react-resizable-panels'; import * as ResizablePrimitive from 'react-resizable-panels';
import { cn } from '../../utils' import { cn } from '../../utils';
const ResizablePanelGroup = ({ const ResizablePanelGroup = ({
className, className,

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import { cn } from '../../utils' import { cn } from '../../utils';
const ScrollArea = React.forwardRef< const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>, React.ElementRef<typeof ScrollAreaPrimitive.Root>,

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import * as SelectPrimitive from '@radix-ui/react-select'; import * as SelectPrimitive from '@radix-ui/react-select';
import { Check, ChevronDown, ChevronUp } from 'lucide-react'; import { Check, ChevronDown, ChevronUp } from 'lucide-react';
import { cn } from '../../utils' import { cn } from '../../utils';
const Select = SelectPrimitive.Root; const Select = SelectPrimitive.Root;

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import * as SeparatorPrimitive from '@radix-ui/react-separator'; import * as SeparatorPrimitive from '@radix-ui/react-separator';
import { cn } from '../../utils' import { cn } from '../../utils';
const Separator = React.forwardRef< const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>, React.ElementRef<typeof SeparatorPrimitive.Root>,

View File

@ -3,7 +3,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import * as React from 'react'; import * as React from 'react';
import { cn } from '../../utils' import { cn } from '../../utils';
const Sheet = SheetPrimitive.Root; const Sheet = SheetPrimitive.Root;

View File

@ -4,7 +4,7 @@ import { VariantProps, cva } from 'class-variance-authority';
import { PanelLeft } from 'lucide-react'; import { PanelLeft } from 'lucide-react';
import { useIsMobile } from '@/hooks/use-mobile'; import { useIsMobile } from '@/hooks/use-mobile';
import { cn } from '../../utils' import { cn } from '../../utils';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import * as SwitchPrimitives from '@radix-ui/react-switch'; import * as SwitchPrimitives from '@radix-ui/react-switch';
import { cn } from '../../utils' import { cn } from '../../utils';
const Switch = React.forwardRef< const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>, React.ElementRef<typeof SwitchPrimitives.Root>,

View File

@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { cn } from '../../utils' import { cn } from '../../utils';
const Table = React.forwardRef< const Table = React.forwardRef<
HTMLTableElement, HTMLTableElement,

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs'; import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from '../../utils' import { cn } from '../../utils';
const Tabs = TabsPrimitive.Root; const Tabs = TabsPrimitive.Root;

View File

@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { cn } from '../../utils' import { cn } from '../../utils';
const Textarea = React.forwardRef< const Textarea = React.forwardRef<
HTMLTextAreaElement, HTMLTextAreaElement,

View File

@ -3,7 +3,7 @@ import * as ToastPrimitives from '@radix-ui/react-toast';
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from 'class-variance-authority';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import { cn } from '../../utils' import { cn } from '../../utils';
const ToastProvider = ToastPrimitives.Provider; const ToastProvider = ToastPrimitives.Provider;

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'; import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
import { type VariantProps } from 'class-variance-authority'; import { type VariantProps } from 'class-variance-authority';
import { cn } from '../../utils' import { cn } from '../../utils';
import { toggleVariants } from '@/components/ui/toggle-variants'; import { toggleVariants } from '@/components/ui/toggle-variants';
const ToggleGroupContext = React.createContext< const ToggleGroupContext = React.createContext<

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import * as TogglePrimitive from '@radix-ui/react-toggle'; import * as TogglePrimitive from '@radix-ui/react-toggle';
import { type VariantProps } from 'class-variance-authority'; import { type VariantProps } from 'class-variance-authority';
import { cn } from '../../utils' import { cn } from '../../utils';
import { toggleVariants } from './toggle-variants'; import { toggleVariants } from './toggle-variants';
const Toggle = React.forwardRef< const Toggle = React.forwardRef<

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import * as TooltipPrimitive from '@radix-ui/react-tooltip'; import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { cn } from '../../utils' import { cn } from '../../utils';
const TooltipProvider = TooltipPrimitive.Provider; const TooltipProvider = TooltipPrimitive.Provider;

View File

@ -39,7 +39,9 @@ export function VerificationStep({
verificationResult?.success && verificationResult?.success &&
verificationResult.message.includes('Checking ownership') verificationResult.message.includes('Checking ownership')
) { ) {
const hasOwnership = currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED; const hasOwnership =
currentUser?.verificationStatus ===
EVerificationStatus.ENS_ORDINAL_VERIFIED;
if (hasOwnership) { if (hasOwnership) {
setVerificationResult({ setVerificationResult({
@ -115,7 +117,9 @@ export function VerificationStep({
}; };
const getVerificationType = () => { const getVerificationType = () => {
return currentUser?.walletType === 'bitcoin' ? 'Bitcoin Ordinal' : 'Ethereum ENS'; return currentUser?.walletType === 'bitcoin'
? 'Bitcoin Ordinal'
: 'Ethereum ENS';
}; };
const getVerificationIcon = () => { const getVerificationIcon = () => {
@ -123,7 +127,9 @@ export function VerificationStep({
}; };
const getVerificationColor = () => { const getVerificationColor = () => {
return currentUser?.walletType === 'bitcoin' ? 'text-orange-500' : 'text-blue-500'; return currentUser?.walletType === 'bitcoin'
? 'text-orange-500'
: 'text-blue-500';
}; };
const getVerificationDescription = () => { const getVerificationDescription = () => {
@ -206,7 +212,9 @@ export function VerificationStep({
} }
// Show verification status // Show verification status
if (currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED) { if (
currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED
) {
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<div className="flex-1 space-y-4"> <div className="flex-1 space-y-4">
@ -222,8 +230,12 @@ export function VerificationStep({
</p> </p>
{currentUser && ( {currentUser && (
<div className="text-xs text-neutral-400"> <div className="text-xs text-neutral-400">
{currentUser?.walletType === 'bitcoin' && <p>Ordinal ID: Verified</p>} {currentUser?.walletType === 'bitcoin' && (
{currentUser?.walletType === 'ethereum' && <p>ENS Name: Verified</p>} <p>Ordinal ID: Verified</p>
)}
{currentUser?.walletType === 'ethereum' && (
<p>ENS Name: Verified</p>
)}
</div> </div>
)} )}
</div> </div>

View File

@ -1,6 +1,6 @@
import { Wifi, WifiOff, CheckCircle } from 'lucide-react'; import { Wifi, WifiOff, CheckCircle } from 'lucide-react';
import { useNetwork } from '@opchan/react'; import { useNetwork } from '@opchan/react';
import { cn } from '../../utils' import { cn } from '../../utils';
interface WakuHealthIndicatorProps { interface WakuHealthIndicatorProps {
className?: string; className?: string;
@ -13,7 +13,7 @@ export function WakuHealthIndicator({
showText = true, showText = true,
size = 'md', size = 'md',
}: WakuHealthIndicatorProps) { }: WakuHealthIndicatorProps) {
const {isConnected, statusMessage} = useNetwork(); const { isConnected, statusMessage } = useNetwork();
const getIcon = () => { const getIcon = () => {
if (isConnected === true) { if (isConnected === true) {
@ -61,7 +61,8 @@ export function WakuHealthIndicator({
*/ */
export function WakuHealthDot({ className }: { className?: string }) { export function WakuHealthDot({ className }: { className?: string }) {
const { isConnected } = useNetwork(); const { isConnected } = useNetwork();
const statusColor = isConnected === true ? 'green' : isConnected === false ? 'red' : 'gray'; const statusColor =
isConnected === true ? 'green' : isConnected === false ? 'red' : 'gray';
return ( return (
<div <div

View File

@ -194,7 +194,9 @@ export function WalletConnectionDialog({
</p> </p>
<p className="text-sm text-neutral-300 mb-2">Address:</p> <p className="text-sm text-neutral-300 mb-2">Address:</p>
<p className="text-xs font-mono text-neutral-400 break-all"> <p className="text-xs font-mono text-neutral-400 break-all">
{activeAddress ? `${activeAddress.slice(0, 6)}...${activeAddress.slice(-4)}` : ''} {activeAddress
? `${activeAddress.slice(0, 6)}...${activeAddress.slice(-4)}`
: ''}
</p> </p>
</div> </div>

View File

@ -29,8 +29,13 @@ export function WalletWizard({
}: WalletWizardProps) { }: WalletWizardProps) {
const [currentStep, setCurrentStep] = React.useState<WizardStep>(1); const [currentStep, setCurrentStep] = React.useState<WizardStep>(1);
const [isLoading, setIsLoading] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false);
const [delegationStatus, setDelegationStatus] = React.useState<boolean>(false); const [delegationStatus, setDelegationStatus] =
const { isAuthenticated, verificationStatus, delegationStatus: getDelegationStatus } = useAuth(); React.useState<boolean>(false);
const {
isAuthenticated,
verificationStatus,
delegationStatus: getDelegationStatus,
} = useAuth();
// Reset wizard when opened - always start at step 1 for simplicity // Reset wizard when opened - always start at step 1 for simplicity
React.useEffect(() => { React.useEffect(() => {
@ -43,9 +48,11 @@ export function WalletWizard({
// Load delegation status when component mounts or when user changes // Load delegation status when component mounts or when user changes
React.useEffect(() => { React.useEffect(() => {
if (isAuthenticated) { if (isAuthenticated) {
getDelegationStatus().then(status => { getDelegationStatus()
setDelegationStatus(status.isValid); .then(status => {
}).catch(console.error); setDelegationStatus(status.isValid);
})
.catch(console.error);
} else { } else {
setDelegationStatus(false); setDelegationStatus(false);
} }

View File

@ -1,11 +1,9 @@
export { export {
useAuth , useAuth,
useForum , useForum,
useNetwork, useNetwork,
usePermissions, usePermissions,
useContent, useContent,
useUIState, useUIState,
useUserDisplay, useUserDisplay,
} from '@opchan/react'; } from '@opchan/react';

View File

@ -14,7 +14,9 @@ if (!(window as Window & typeof globalThis).Buffer) {
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<WagmiProvider config={config}> <WagmiProvider config={config}>
<AppKitProvider {...appkitConfig}> <AppKitProvider {...appkitConfig}>
<OpchanWithAppKit config={{ ordiscanApiKey: '6bb07766-d98c-4ddd-93fb-6a0e94d629dd' }}> <OpchanWithAppKit
config={{ ordiscanApiKey: '6bb07766-d98c-4ddd-93fb-6a0e94d629dd' }}
>
<App /> <App />
</OpchanWithAppKit> </OpchanWithAppKit>
</AppKitProvider> </AppKitProvider>

View File

@ -53,8 +53,12 @@ const BookmarksPage = () => {
); );
} }
const postBookmarks = bookmarks.filter(bookmark => bookmark.type === BookmarkType.POST); const postBookmarks = bookmarks.filter(
const commentBookmarks = bookmarks.filter(bookmark => bookmark.type === BookmarkType.COMMENT); bookmark => bookmark.type === BookmarkType.POST
);
const commentBookmarks = bookmarks.filter(
bookmark => bookmark.type === BookmarkType.COMMENT
);
const getFilteredBookmarks = () => { const getFilteredBookmarks = () => {
switch (activeTab) { switch (activeTab) {
@ -79,7 +83,6 @@ const BookmarksPage = () => {
await clearAllBookmarks(); await clearAllBookmarks();
}; };
return ( return (
<div className="page-container"> <div className="page-container">
<Header /> <Header />

View File

@ -20,15 +20,17 @@ const FeedPage: React.FC = () => {
const { verificationStatus } = useAuth(); const { verificationStatus } = useAuth();
const [sortOption, setSortOption] = useState<SortOption>('relevance'); const [sortOption, setSortOption] = useState<SortOption>('relevance');
const allPosts = useMemo(
// Build sorted posts from content slices () => sortPosts([...content.posts], sortOption),
const allPosts = useMemo(() => sortPosts([...content.posts], sortOption), [content.posts, sortOption]); [content.posts, sortOption]
);
// ✅ Get comment count from filtered organized data
const getCommentCount = (postId: string) => (content.commentsByPost[postId] || []).length;
// Loading skeleton // Loading skeleton
if (!content.posts.length && !content.comments.length && !content.cells.length) { if (
!content.posts.length &&
!content.comments.length &&
!content.cells.length
) {
return ( return (
<div className="min-h-screen bg-cyber-dark"> <div className="min-h-screen bg-cyber-dark">
<div className="container mx-auto px-4 py-6 max-w-6xl"> <div className="container mx-auto px-4 py-6 max-w-6xl">
@ -151,20 +153,13 @@ const FeedPage: React.FC = () => {
{verificationStatus !== {verificationStatus !==
EVerificationStatus.ENS_ORDINAL_VERIFIED && ( EVerificationStatus.ENS_ORDINAL_VERIFIED && (
<p className="text-sm text-cyber-neutral/80"> <p className="text-sm text-cyber-neutral/80">
Connect your wallet to Connect your wallet to start posting
start posting
</p> </p>
)} )}
</div> </div>
</div> </div>
) : ( ) : (
allPosts.map(post => ( allPosts.map(post => <PostCard key={post.id} post={post} />)
<PostCard
key={post.id}
post={post}
commentCount={getCommentCount(post.id)}
/>
))
)} )}
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
import { useState} from 'react'; import { useState } from 'react';
import { useForum } from '@opchan/react'; import { useForum } from '@opchan/react';
import { useAuth } from '@opchan/react'; import { useAuth } from '@opchan/react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -27,8 +27,7 @@ import {
Globe, Globe,
Edit3, Edit3,
Save, Save,
X, X,
} from 'lucide-react'; } from 'lucide-react';
import { EDisplayPreference, EVerificationStatus } from '@opchan/core'; import { EDisplayPreference, EVerificationStatus } from '@opchan/core';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
@ -42,7 +41,6 @@ export default function ProfilePage() {
// Get current user from auth context for the address // Get current user from auth context for the address
const { currentUser, delegationInfo } = useAuth(); const { currentUser, delegationInfo } = useAuth();
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [callSign, setCallSign] = useState(''); const [callSign, setCallSign] = useState('');
@ -253,36 +251,31 @@ export default function ProfilePage() {
</div> </div>
<div className="text-sm text-cyber-neutral"> <div className="text-sm text-cyber-neutral">
{/* Show ENS name if available */} {/* Show ENS name if available */}
{(currentUser.ensDetails?.ensName ) && ( {currentUser.ensDetails?.ensName && (
<div> <div>ENS: {currentUser.ensDetails?.ensName}</div>
ENS:{' '}
{currentUser.ensDetails?.ensName}
</div>
)} )}
{/* Show Ordinal details if available */} {/* Show Ordinal details if available */}
{(currentUser.ordinalDetails ) && ( {currentUser.ordinalDetails && (
<div> <div>
Ordinal:{' '} Ordinal:{' '}
{currentUser.ordinalDetails.ordinalDetails} {currentUser.ordinalDetails.ordinalDetails}
</div> </div>
)} )}
{/* Show fallback if neither ENS nor Ordinal */} {/* Show fallback if neither ENS nor Ordinal */}
{!( {!currentUser.ensDetails?.ensName &&
currentUser.ensDetails?.ensName !currentUser.ordinalDetails?.ordinalDetails && (
) && <div>No ENS or Ordinal verification</div>
!( )}
currentUser.ordinalDetails?.ordinalDetails <div className="flex items-center gap-2 mt-2">
) && <div>No ENS or Ordinal verification</div>} {getVerificationIcon()}
<div className="flex items-center gap-2 mt-2"> <Badge className={getVerificationColor()}>
{getVerificationIcon()} {getVerificationText()}
<Badge className={getVerificationColor()}> </Badge>
{getVerificationText()} </div>
</Badge>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
{/* Wallet Section */} {/* Wallet Section */}
<div className="space-y-3"> <div className="space-y-3">
@ -469,7 +462,9 @@ export default function ProfilePage() {
Delegation Delegation
</span> </span>
<Badge <Badge
variant={delegationInfo.isValid ? 'default' : 'secondary'} variant={
delegationInfo.isValid ? 'default' : 'secondary'
}
className={ className={
delegationInfo.isValid delegationInfo.isValid
? 'bg-green-500/20 text-green-400 border-green-500/30' ? 'bg-green-500/20 text-green-400 border-green-500/30'
@ -540,17 +535,17 @@ export default function ProfilePage() {
</div> </div>
{/* Warning for expired delegation */} {/* Warning for expired delegation */}
{(!delegationInfo.isValid && delegationInfo.hasDelegation) && ( {!delegationInfo.isValid && delegationInfo.hasDelegation && (
<div className="p-3 bg-orange-500/10 border border-orange-500/30 rounded-md"> <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"> <div className="flex items-center gap-2 text-orange-400">
<AlertTriangle className="w-4 h-4" /> <AlertTriangle className="w-4 h-4" />
<span className="text-xs font-medium"> <span className="text-xs font-medium">
Delegation expired. Renew to continue using your Delegation expired. Renew to continue using your
browser key. browser key.
</span> </span>
</div> </div>
</div> </div>
)} )}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@ -1,37 +1,57 @@
import * as React from 'react'; import * as React from 'react';
import { OpChanProvider, type WalletAdapter, type WalletAdapterAccount } from '@opchan/react'; import {
OpChanProvider,
type WalletAdapter,
type WalletAdapterAccount,
} from '@opchan/react';
import { useAppKitAccount, modal } from '@reown/appkit/react'; import { useAppKitAccount, modal } from '@reown/appkit/react';
import { AppKit } from '@reown/appkit'; import { AppKit } from '@reown/appkit';
import type { OpChanClientConfig } from '@opchan/core'; import type { OpChanClientConfig } from '@opchan/core';
import { walletManager } from '@opchan/core'; import { walletManager } from '@opchan/core';
interface Props { config: OpChanClientConfig; children: React.ReactNode } interface Props {
config: OpChanClientConfig;
children: React.ReactNode;
}
export const OpchanWithAppKit: React.FC<Props> = ({ config, children }) => { export const OpchanWithAppKit: React.FC<Props> = ({ config, children }) => {
const btc = useAppKitAccount({ namespace: 'bip122' }); const btc = useAppKitAccount({ namespace: 'bip122' });
const eth = useAppKitAccount({ namespace: 'eip155' }); const eth = useAppKitAccount({ namespace: 'eip155' });
const listenersRef = React.useRef(new Set<(a: WalletAdapterAccount | null) => void>()); const listenersRef = React.useRef(
new Set<(a: WalletAdapterAccount | null) => void>()
);
const getCurrent = React.useCallback((): WalletAdapterAccount | null => { const getCurrent = React.useCallback((): WalletAdapterAccount | null => {
if (btc.isConnected && btc.address) return { address: btc.address, walletType: 'bitcoin' }; if (btc.isConnected && btc.address)
if (eth.isConnected && eth.address) return { address: eth.address, walletType: 'ethereum' }; return { address: btc.address, walletType: 'bitcoin' };
if (eth.isConnected && eth.address)
return { address: eth.address, walletType: 'ethereum' };
return null; return null;
}, [btc.isConnected, btc.address, eth.isConnected, eth.address]); }, [btc.isConnected, btc.address, eth.isConnected, eth.address]);
const adapter = React.useMemo<WalletAdapter>(() => ({ const adapter = React.useMemo<WalletAdapter>(
getAccount: () => getCurrent(), () => ({
onChange: (cb) => { getAccount: () => getCurrent(),
listenersRef.current.add(cb); onChange: cb => {
return () => { listenersRef.current.delete(cb); }; listenersRef.current.add(cb);
}, return () => {
}), [getCurrent]); listenersRef.current.delete(cb);
};
},
}),
[getCurrent]
);
// Notify listeners when AppKit account changes // Notify listeners when AppKit account changes
React.useEffect(() => { React.useEffect(() => {
const account = getCurrent(); const account = getCurrent();
listenersRef.current.forEach(cb => { listenersRef.current.forEach(cb => {
try { cb(account); } catch { /* ignore */ } try {
cb(account);
} catch {
/* ignore */
}
}); });
}, [getCurrent]); }, [getCurrent]);
@ -56,5 +76,3 @@ export const OpchanWithAppKit: React.FC<Props> = ({ config, children }) => {
</OpChanProvider> </OpChanProvider>
); );
}; };

View File

@ -1,9 +1,8 @@
import { clsx, type ClassValue } from 'clsx'; import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
} }
export { urlLoads } from './urlLoads'; export { urlLoads } from './urlLoads';