mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 12:53:10 +00:00
chore: linting + improve CSS
This commit is contained in:
parent
b6e78ac71c
commit
f9863121ba
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useForumData } from '@/hooks';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useForum } from '@/hooks';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
@ -35,54 +35,48 @@ interface CommentFeedItem extends FeedItemBase {
|
||||
type FeedItem = PostFeedItem | CommentFeedItem;
|
||||
|
||||
const ActivityFeed: React.FC = () => {
|
||||
// ✅ Use reactive hooks for data
|
||||
const forumData = useForumData();
|
||||
const { content, network } = useForum();
|
||||
|
||||
const {
|
||||
postsWithVoteStatus,
|
||||
commentsWithVoteStatus,
|
||||
cellsWithStats,
|
||||
isInitialLoading,
|
||||
} = forumData;
|
||||
const { posts, comments, cells, commentsByPost } = content;
|
||||
const { isConnected } = network;
|
||||
|
||||
// ✅ Use pre-computed data with vote scores
|
||||
const combinedFeed: FeedItem[] = [
|
||||
...postsWithVoteStatus.map(
|
||||
(post): PostFeedItem => ({
|
||||
id: post.id,
|
||||
type: 'post',
|
||||
timestamp: post.timestamp,
|
||||
ownerAddress: post.author,
|
||||
title: post.title,
|
||||
cellId: post.cellId,
|
||||
postId: post.id,
|
||||
commentCount: forumData.commentsByPost[post.id]?.length || 0,
|
||||
voteCount: post.voteScore,
|
||||
})
|
||||
),
|
||||
...commentsWithVoteStatus
|
||||
.map((comment): CommentFeedItem | null => {
|
||||
const parentPost = postsWithVoteStatus.find(
|
||||
p => p.id === comment.postId
|
||||
);
|
||||
if (!parentPost) return null;
|
||||
return {
|
||||
id: comment.id,
|
||||
type: 'comment',
|
||||
timestamp: comment.timestamp,
|
||||
ownerAddress: comment.author,
|
||||
content: comment.content,
|
||||
postId: comment.postId,
|
||||
cellId: parentPost.cellId,
|
||||
voteCount: comment.voteScore,
|
||||
};
|
||||
})
|
||||
.filter((item): item is CommentFeedItem => item !== null),
|
||||
].sort((a, b) => b.timestamp - a.timestamp);
|
||||
|
||||
const combinedFeed: FeedItem[] = useMemo(() => {
|
||||
return [
|
||||
...posts.map(
|
||||
(post): PostFeedItem => ({
|
||||
...post,
|
||||
type: 'post',
|
||||
ownerAddress: post.authorAddress,
|
||||
cellId: post.cellId,
|
||||
postId: post.id,
|
||||
title: post.title,
|
||||
commentCount: commentsByPost[post.id]?.length || 0,
|
||||
voteCount: post.upvotes.length - post.downvotes.length,
|
||||
})
|
||||
),
|
||||
...comments
|
||||
.map((comment): CommentFeedItem | null => {
|
||||
const parentPost = posts.find(p => p.id === comment.postId);
|
||||
if (!parentPost) return null;
|
||||
return {
|
||||
id: comment.id,
|
||||
type: 'comment',
|
||||
timestamp: comment.timestamp,
|
||||
ownerAddress: comment.author,
|
||||
content: comment.content,
|
||||
postId: comment.postId,
|
||||
cellId: parentPost.cellId,
|
||||
voteCount: comment.upvotes.length - comment.downvotes.length,
|
||||
};
|
||||
})
|
||||
.filter((item): item is CommentFeedItem => item !== null),
|
||||
].sort((a, b) => b.timestamp - a.timestamp);
|
||||
}, [posts, comments, commentsByPost]);
|
||||
|
||||
const renderFeedItem = (item: FeedItem) => {
|
||||
const cell = item.cellId
|
||||
? cellsWithStats.find(c => c.id === item.cellId)
|
||||
? cells.find(c => c.id === item.cellId)
|
||||
: undefined;
|
||||
const timeAgo = formatDistanceToNow(new Date(item.timestamp), {
|
||||
addSuffix: true,
|
||||
@ -150,7 +144,7 @@ const ActivityFeed: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
if (isInitialLoading) {
|
||||
if (!isConnected) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useContent, usePermissions } from '@/hooks';
|
||||
import { useContent, usePermissions } from '@/hooks';
|
||||
import {
|
||||
Layout,
|
||||
MessageSquare,
|
||||
@ -226,9 +226,7 @@ const CellList = () => {
|
||||
title="Refresh data"
|
||||
className="px-3 border-cyber-muted/30 text-cyber-neutral hover:bg-cyber-muted/30"
|
||||
>
|
||||
<RefreshCw
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
{canCreateCell && <CreateCellDialog />}
|
||||
|
||||
@ -61,10 +61,14 @@ const CommentCard: React.FC<CommentCardProps> = ({
|
||||
// Use library pending API
|
||||
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 userDownvoted = Boolean(comment.downvotes.some(v => v.author === currentUser?.address));
|
||||
const userUpvoted = Boolean(comment.upvotes.some(v => v.author === currentUser?.address));
|
||||
const userDownvoted = Boolean(
|
||||
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 handleVoteComment = async (isUpvote: boolean) => {
|
||||
@ -86,7 +90,9 @@ const CommentCard: React.FC<CommentCardProps> = ({
|
||||
<div className="flex flex-col items-center">
|
||||
<button
|
||||
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)}
|
||||
disabled={!permissions.canVote}
|
||||
@ -99,7 +105,9 @@ const CommentCard: React.FC<CommentCardProps> = ({
|
||||
<span className="text-sm font-bold">{score}</span>
|
||||
<button
|
||||
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)}
|
||||
disabled={!permissions.canVote}
|
||||
|
||||
@ -79,7 +79,8 @@ export function CreateCellDialog({
|
||||
if (!canCreateCell) {
|
||||
toast({
|
||||
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',
|
||||
});
|
||||
return;
|
||||
|
||||
@ -4,16 +4,15 @@ import { TrendingUp, Users, Eye, CheckCircle } from 'lucide-react';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useAuth, useContent } from '@/hooks';
|
||||
import { useAuth, useContent } from '@/hooks';
|
||||
import { EVerificationStatus } from '@opchan/core';
|
||||
import { CypherImage } from '@/components/ui/CypherImage';
|
||||
|
||||
const FeedSidebar: React.FC = () => {
|
||||
const {cells, posts, comments, cellsWithStats, userVerificationStatus} = useContent();
|
||||
const { cells, posts, comments, cellsWithStats, userVerificationStatus } =
|
||||
useContent();
|
||||
const { currentUser, verificationStatus } = useAuth();
|
||||
|
||||
|
||||
|
||||
const stats = {
|
||||
totalCells: cells.length,
|
||||
totalPosts: posts.length,
|
||||
@ -61,7 +60,9 @@ const FeedSidebar: React.FC = () => {
|
||||
<Users className="w-5 h-5 text-cyber-accent" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{currentUser?.displayName}</div>
|
||||
<div className="font-medium text-sm">
|
||||
{currentUser?.displayName}
|
||||
</div>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={`${verificationBadge.color} text-white text-xs`}
|
||||
|
||||
@ -49,9 +49,9 @@ import { WakuHealthDot } from '@/components/ui/waku-health-indicator';
|
||||
|
||||
const Header = () => {
|
||||
const { currentUser, delegationInfo } = useAuth();
|
||||
const {statusMessage} = useNetwork();
|
||||
const { statusMessage } = useNetwork();
|
||||
|
||||
const location = useLocation()
|
||||
const location = useLocation();
|
||||
const { toast } = useToast();
|
||||
const { content } = useForum();
|
||||
|
||||
@ -65,7 +65,10 @@ const Header = () => {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
// 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
|
||||
React.useEffect(() => {
|
||||
@ -109,23 +112,26 @@ const Header = () => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
console.log('currentUser', currentUser)
|
||||
}, [currentUser])
|
||||
console.log('currentUser', currentUser);
|
||||
}, [currentUser]);
|
||||
|
||||
const getStatusIcon = () => {
|
||||
if (!isConnected) return <CircleSlash className="w-4 h-4" />;
|
||||
|
||||
if (
|
||||
currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED &&
|
||||
currentUser?.verificationStatus ===
|
||||
EVerificationStatus.ENS_ORDINAL_VERIFIED &&
|
||||
delegationInfo?.isValid
|
||||
) {
|
||||
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" />;
|
||||
} else if (
|
||||
currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED
|
||||
currentUser?.verificationStatus ===
|
||||
EVerificationStatus.ENS_ORDINAL_VERIFIED
|
||||
) {
|
||||
return <Key className="w-4 h-4" />;
|
||||
} else {
|
||||
@ -197,11 +203,13 @@ const Header = () => {
|
||||
>
|
||||
{getStatusIcon()}
|
||||
<span className="ml-1">
|
||||
{currentUser?.verificationStatus === EVerificationStatus.WALLET_UNCONNECTED
|
||||
{currentUser?.verificationStatus ===
|
||||
EVerificationStatus.WALLET_UNCONNECTED
|
||||
? 'CONNECT'
|
||||
: delegationInfo?.isValid
|
||||
? 'READY'
|
||||
: currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED
|
||||
: currentUser?.verificationStatus ===
|
||||
EVerificationStatus.ENS_ORDINAL_VERIFIED
|
||||
? 'EXPIRED'
|
||||
: 'DELEGATE'}
|
||||
</span>
|
||||
@ -215,7 +223,9 @@ const Header = () => {
|
||||
size="sm"
|
||||
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" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@ -2,59 +2,52 @@ import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ArrowUp, ArrowDown, MessageSquare } from 'lucide-react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import type { Post, PostMessage } from '@opchan/core';
|
||||
import type { Post } from '@opchan/core';
|
||||
import { RelevanceIndicator } from '@/components/ui/relevance-indicator';
|
||||
import { AuthorDisplay } from '@/components/ui/author-display';
|
||||
import { BookmarkButton } from '@/components/ui/bookmark-button';
|
||||
import { LinkRenderer } from '@/components/ui/link-renderer';
|
||||
import { useContent, usePermissions } from '@/hooks';
|
||||
import { useAuth, useContent, usePermissions } from '@/hooks';
|
||||
import { ShareButton } from '@/components/ui/ShareButton';
|
||||
|
||||
interface PostCardProps {
|
||||
post: Post | PostMessage;
|
||||
commentCount?: number;
|
||||
post: Post;
|
||||
}
|
||||
|
||||
const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => {
|
||||
const content = useContent();
|
||||
const PostCard: React.FC<PostCardProps> = ({ post }) => {
|
||||
const {
|
||||
bookmarks,
|
||||
pending,
|
||||
vote,
|
||||
togglePostBookmark,
|
||||
cells,
|
||||
commentsByPost,
|
||||
} = useContent();
|
||||
const permissions = usePermissions();
|
||||
const { currentUser } = useAuth();
|
||||
|
||||
// Get cell data from content
|
||||
const cell = content.cells.find((c) => c.id === post.cellId);
|
||||
const cellName = cell?.name || 'unknown';
|
||||
const cellName = cells.find(c => c.id === post.cellId)?.name || 'unknown';
|
||||
const commentCount = commentsByPost[post.id]?.length || 0;
|
||||
|
||||
// Use pre-computed vote data or safely compute from arrays when available
|
||||
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;
|
||||
const isPending = pending.isPending(post.id);
|
||||
|
||||
// Use library pending API
|
||||
const isPending = content.pending.isPending(post.id);
|
||||
|
||||
// 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 isBookmarked = bookmarks.some(
|
||||
b => b.targetId === post.id && b.type === 'post'
|
||||
);
|
||||
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 = typeof post.content === 'string' ? post.content : String(post.content ?? '');
|
||||
const contentText =
|
||||
typeof post.content === 'string'
|
||||
? post.content
|
||||
: String(post.content ?? '');
|
||||
const contentPreview =
|
||||
contentText.length > 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) => {
|
||||
e.preventDefault();
|
||||
await content.vote({ targetId: post.id, isUpvote });
|
||||
await vote({ targetId: post.id, isUpvote });
|
||||
};
|
||||
|
||||
const handleBookmark = async (e?: React.MouseEvent) => {
|
||||
@ -72,7 +65,7 @@ const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => {
|
||||
}
|
||||
setBookmarkLoading(true);
|
||||
try {
|
||||
await content.togglePostBookmark(post, post.cellId);
|
||||
await togglePostBookmark(post, post.cellId);
|
||||
} finally {
|
||||
setBookmarkLoading(false);
|
||||
}
|
||||
@ -97,13 +90,7 @@ const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => {
|
||||
</button>
|
||||
|
||||
<span
|
||||
className={`text-sm font-medium px-1 ${
|
||||
score > 0
|
||||
? 'text-cyber-accent'
|
||||
: score < 0
|
||||
? 'text-blue-400'
|
||||
: 'text-cyber-neutral'
|
||||
}`}
|
||||
className={`text-sm font-medium px-1`}
|
||||
>
|
||||
{score}
|
||||
</span>
|
||||
@ -130,9 +117,17 @@ const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => {
|
||||
<div 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>
|
||||
<Link
|
||||
to={cellName ? `/cell/${post.cellId}` : "#"}
|
||||
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>Posted by u/</span>
|
||||
<AuthorDisplay
|
||||
@ -146,18 +141,23 @@ const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</span>
|
||||
{('relevanceScore' in post) && typeof (post as Post).relevanceScore === 'number' && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<RelevanceIndicator
|
||||
score={(post as Post).relevanceScore as number}
|
||||
details={('relevanceDetails' in post ? (post as Post).relevanceDetails : undefined)}
|
||||
type="post"
|
||||
className="text-xs"
|
||||
showTooltip={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{'relevanceScore' in post &&
|
||||
typeof (post as Post).relevanceScore === 'number' && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<RelevanceIndicator
|
||||
score={(post as Post).relevanceScore as number}
|
||||
details={
|
||||
'relevanceDetails' in post
|
||||
? (post as Post).relevanceDetails
|
||||
: undefined
|
||||
}
|
||||
type="post"
|
||||
className="text-xs"
|
||||
showTooltip={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Post title and content - clickable to navigate to post */}
|
||||
|
||||
@ -17,7 +17,7 @@ import { AuthorDisplay } from './ui/author-display';
|
||||
import { BookmarkButton } from './ui/bookmark-button';
|
||||
import { MarkdownRenderer } from './ui/markdown-renderer';
|
||||
import CommentCard from './CommentCard';
|
||||
import { useContent, usePermissions } from '@/hooks';
|
||||
import { useAuth, useContent, usePermissions } from '@/hooks';
|
||||
import type { Cell as ForumCell } from '@opchan/core';
|
||||
import { ShareButton } from './ui/ShareButton';
|
||||
|
||||
@ -28,17 +28,19 @@ const PostDetail = () => {
|
||||
// Use aggregated forum API
|
||||
const content = useContent();
|
||||
const permissions = usePermissions();
|
||||
|
||||
const { currentUser } = useAuth();
|
||||
// Get post and comments using focused hooks
|
||||
const post = content.posts.find((p) => p.id === postId);
|
||||
const visibleComments = postId ? content.commentsByPost[postId] ?? [] : [];
|
||||
const post = content.posts.find(p => p.id === postId);
|
||||
const visibleComments = postId ? (content.commentsByPost[postId] ?? []) : [];
|
||||
|
||||
// Use library pending API
|
||||
const postPending = content.pending.isPending(post?.id);
|
||||
const postVotePending = content.pending.isPending(post?.id);
|
||||
|
||||
// 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 [newComment, setNewComment] = useState('');
|
||||
@ -115,11 +117,13 @@ const PostDetail = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Get vote status from post data (enhanced posts only)
|
||||
const enhanced = post as unknown as { userUpvoted?: boolean; userDownvoted?: boolean; voteScore?: number };
|
||||
const isPostUpvoted = Boolean(enhanced.userUpvoted);
|
||||
const isPostDownvoted = Boolean(enhanced.userDownvoted);
|
||||
const score = typeof enhanced.voteScore === 'number' ? enhanced.voteScore : 0;
|
||||
const score = post.upvotes.length - post.downvotes.length;
|
||||
const isPostUpvoted = Boolean(
|
||||
post.upvotes.some(v => v.author === currentUser?.address)
|
||||
);
|
||||
const isPostDownvoted = Boolean(
|
||||
post.downvotes.some(v => v.author === currentUser?.address)
|
||||
);
|
||||
|
||||
const handleModerateComment = async (commentId: string) => {
|
||||
const reason =
|
||||
@ -160,7 +164,9 @@ const PostDetail = () => {
|
||||
<div className="flex flex-col items-center">
|
||||
<button
|
||||
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)}
|
||||
disabled={!permissions.canVote}
|
||||
@ -173,7 +179,9 @@ const PostDetail = () => {
|
||||
<span className="text-sm font-bold">{score}</span>
|
||||
<button
|
||||
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)}
|
||||
disabled={!permissions.canVote}
|
||||
@ -194,9 +202,17 @@ const PostDetail = () => {
|
||||
|
||||
<div className="flex-1">
|
||||
<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'}
|
||||
</span>
|
||||
</Link>
|
||||
<span>•</span>
|
||||
<span>Posted by u/</span>
|
||||
<AuthorDisplay
|
||||
@ -279,9 +295,7 @@ const PostDetail = () => {
|
||||
|
||||
{!permissions.canComment && (
|
||||
<div className="mb-6 p-4 border border-cyber-muted rounded-sm bg-cyber-muted/20 text-center">
|
||||
<p className="text-sm mb-3">
|
||||
Connect your wallet to comment
|
||||
</p>
|
||||
<p className="text-sm mb-3">Connect your wallet to comment</p>
|
||||
<Button asChild size="sm">
|
||||
<Link to="/">Connect Wallet</Link>
|
||||
</Button>
|
||||
@ -306,7 +320,7 @@ const PostDetail = () => {
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
visibleComments.map((comment) => (
|
||||
visibleComments.map(comment => (
|
||||
<CommentCard
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
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 { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
@ -31,8 +35,8 @@ import {
|
||||
const PostList = () => {
|
||||
const { cellId } = useParams<{ cellId: string }>();
|
||||
|
||||
// ✅ Use reactive hooks for data and actions
|
||||
const { createPost, vote, moderate, refresh, commentsByPost, cells, posts } = useContent();
|
||||
const { createPost, vote, moderate, refresh, commentsByPost, cells, posts } =
|
||||
useContent();
|
||||
const cell = cells.find((c: ForumCell) => c.id === cellId);
|
||||
const isCreatingPost = false;
|
||||
const isVoting = false;
|
||||
@ -101,7 +105,11 @@ const PostList = () => {
|
||||
if (!newPostContent.trim()) return;
|
||||
|
||||
// ✅ 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) {
|
||||
setNewPostTitle('');
|
||||
setNewPostContent('');
|
||||
@ -129,8 +137,12 @@ const PostList = () => {
|
||||
if (!currentUser) return null;
|
||||
const p = posts.find((p: ForumPost) => p.id === postId);
|
||||
if (!p) return null;
|
||||
const up = p.upvotes.some((v: VoteMessage) => v.author === currentUser.address);
|
||||
const down = p.downvotes.some((v: VoteMessage) => v.author === currentUser.address);
|
||||
const up = p.upvotes.some(
|
||||
(v: VoteMessage) => v.author === currentUser.address
|
||||
);
|
||||
const down = p.downvotes.some(
|
||||
(v: VoteMessage) => v.author === currentUser.address
|
||||
);
|
||||
return up ? 'upvote' : down ? 'downvote' : null;
|
||||
};
|
||||
|
||||
@ -248,9 +260,7 @@ const PostList = () => {
|
||||
|
||||
{!canPost && !currentUser && (
|
||||
<div className="section-spacing content-card-sm text-center">
|
||||
<p className="text-sm mb-3">
|
||||
Connect your wallet to post
|
||||
</p>
|
||||
<p className="text-sm mb-3">Connect your wallet to post</p>
|
||||
<Button asChild size="sm">
|
||||
<Link to="/">Connect Wallet</Link>
|
||||
</Button>
|
||||
@ -277,9 +287,7 @@ const PostList = () => {
|
||||
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${getPostVoteType(post.id) === 'upvote' ? 'text-cyber-accent' : ''}`}
|
||||
onClick={() => handleVotePost(post.id, true)}
|
||||
disabled={!canVote || isVoting}
|
||||
title={
|
||||
canVote ? 'Upvote' : 'Connect your wallet to vote'
|
||||
}
|
||||
title={canVote ? 'Upvote' : 'Connect your wallet to vote'}
|
||||
>
|
||||
<ArrowUp className="w-4 h-4" />
|
||||
</button>
|
||||
@ -290,9 +298,7 @@ const PostList = () => {
|
||||
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${getPostVoteType(post.id) === 'downvote' ? 'text-cyber-accent' : ''}`}
|
||||
onClick={() => handleVotePost(post.id, false)}
|
||||
disabled={!canVote || isVoting}
|
||||
title={
|
||||
canVote ? 'Downvote' : 'Connect your wallet to vote'
|
||||
}
|
||||
title={canVote ? 'Downvote' : 'Connect your wallet to vote'}
|
||||
>
|
||||
<ArrowDown className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
type CypherImageProps = {
|
||||
src?: string;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Share2 } from 'lucide-react';
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
import { useToast } from '../ui/use-toast';
|
||||
|
||||
interface ShareButtonProps {
|
||||
|
||||
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
import { buttonVariants } from '@/components/ui/button-variants';
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
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',
|
||||
|
||||
@ -14,11 +14,12 @@ export function AuthorDisplay({
|
||||
className = '',
|
||||
showBadge = true,
|
||||
}: AuthorDisplayProps) {
|
||||
const { ensName, ordinalDetails, callSign, displayName } = useUserDisplay(address);
|
||||
const { ensName, ordinalDetails, callSign, displayName } =
|
||||
useUserDisplay(address);
|
||||
|
||||
useEffect(()=> {
|
||||
console.log({ensName, ordinalDetails, callSign, displayName, address})
|
||||
}, [address, ensName, ordinalDetails, callSign, displayName])
|
||||
useEffect(() => {
|
||||
console.log({ ensName, ordinalDetails, callSign, displayName, address });
|
||||
}, [address, ensName, ordinalDetails, callSign, displayName]);
|
||||
|
||||
// Only show a badge if the author has ENS, Ordinal, or Call Sign
|
||||
const shouldShowBadge = showBadge && (ensName || ordinalDetails || callSign);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
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',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Bookmark, BookmarkCheck } from 'lucide-react';
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
interface BookmarkButtonProps {
|
||||
isBookmarked: boolean;
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { Bookmark, BookmarkType } from '@opchan/core';
|
||||
import { useUserDisplay } from '@opchan/react';
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { ChevronRight, MoreHorizontal } from 'lucide-react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const Breadcrumb = React.forwardRef<
|
||||
HTMLElement,
|
||||
|
||||
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
import { buttonVariants } from './button-variants';
|
||||
|
||||
export interface ButtonProps
|
||||
|
||||
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { DayPicker } from 'react-day-picker';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
import { buttonVariants } from '@/components/ui/button-variants';
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
|
||||
@ -4,7 +4,7 @@ import useEmblaCarousel, {
|
||||
} from 'embla-carousel-react';
|
||||
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import * as RechartsPrimitive from 'recharts';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: '', dark: '.dark' } as const;
|
||||
|
||||
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||
import { Check } from 'lucide-react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
|
||||
@ -3,7 +3,7 @@ import { type DialogProps } from '@radix-ui/react-dialog';
|
||||
import { Command as CommandPrimitive } from 'cmdk';
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog';
|
||||
|
||||
const Command = React.forwardRef<
|
||||
|
||||
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
|
||||
import { Check, ChevronRight, Circle } from 'lucide-react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const ContextMenu = ContextMenuPrimitive.Root;
|
||||
|
||||
|
||||
@ -194,7 +194,9 @@ export function DelegationStep({
|
||||
{/* User Address */}
|
||||
{currentUser && (
|
||||
<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>
|
||||
)}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { Drawer as DrawerPrimitive } from 'vaul';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const Drawer = ({
|
||||
shouldScaleBackground = true,
|
||||
|
||||
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||
import { Check, ChevronRight, Circle } from 'lucide-react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
useFormContext,
|
||||
} from 'react-hook-form';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root;
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import { OTPInput, OTPInputContext } from 'input-otp';
|
||||
import { Dot } from 'lucide-react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const InputOTP = React.forwardRef<
|
||||
React.ElementRef<typeof OTPInput>,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
|
||||
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const labelVariants = cva(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
|
||||
|
||||
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import * as MenubarPrimitive from '@radix-ui/react-menubar';
|
||||
import { Check, ChevronRight, Circle } from 'lucide-react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const MenubarMenu = MenubarPrimitive.Menu;
|
||||
|
||||
|
||||
@ -8,8 +8,14 @@ export function ModerationToggle() {
|
||||
const { canModerate } = usePermissions();
|
||||
const { cellsWithStats } = useContent();
|
||||
|
||||
const [showModerated, setShowModerated] = useUIState<boolean>('showModerated', false);
|
||||
const toggleShowModerated = React.useCallback((value: boolean) => setShowModerated(value), [setShowModerated]);
|
||||
const [showModerated, setShowModerated] = useUIState<boolean>(
|
||||
'showModerated',
|
||||
false
|
||||
);
|
||||
const toggleShowModerated = React.useCallback(
|
||||
(value: boolean) => setShowModerated(value),
|
||||
[setShowModerated]
|
||||
);
|
||||
|
||||
// Check if user is admin of any cell
|
||||
const isAdminOfAnyCell = cellsWithStats.some(cell => canModerate(cell.id));
|
||||
|
||||
@ -3,7 +3,7 @@ import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const NavigationMenu = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
import { ButtonProps } from '@/components/ui/button';
|
||||
import { buttonVariants } from '@/components/ui/button-variants';
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import * as ProgressPrimitive from '@radix-ui/react-progress';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
|
||||
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
|
||||
import { Circle } from 'lucide-react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const RadioGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { Resizable } from 're-resizable';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
|
||||
type ResizableTextareaProps =
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { GripVertical } from 'lucide-react';
|
||||
import * as ResizablePrimitive from 'react-resizable-panels';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const ResizablePanelGroup = ({
|
||||
className,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
|
||||
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||
import { Check, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
|
||||
@ -3,7 +3,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { X } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import { VariantProps, cva } from 'class-variance-authority';
|
||||
import { PanelLeft } from 'lucide-react';
|
||||
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import * as SwitchPrimitives from '@radix-ui/react-switch';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const Textarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
|
||||
@ -3,7 +3,7 @@ import * as ToastPrimitives from '@radix-ui/react-toast';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider;
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
|
||||
import { type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
import { toggleVariants } from '@/components/ui/toggle-variants';
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
|
||||
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import * as TogglePrimitive from '@radix-ui/react-toggle';
|
||||
import { type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
import { toggleVariants } from './toggle-variants';
|
||||
|
||||
const Toggle = React.forwardRef<
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
|
||||
@ -39,7 +39,9 @@ export function VerificationStep({
|
||||
verificationResult?.success &&
|
||||
verificationResult.message.includes('Checking ownership')
|
||||
) {
|
||||
const hasOwnership = currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED;
|
||||
const hasOwnership =
|
||||
currentUser?.verificationStatus ===
|
||||
EVerificationStatus.ENS_ORDINAL_VERIFIED;
|
||||
|
||||
if (hasOwnership) {
|
||||
setVerificationResult({
|
||||
@ -115,7 +117,9 @@ export function VerificationStep({
|
||||
};
|
||||
|
||||
const getVerificationType = () => {
|
||||
return currentUser?.walletType === 'bitcoin' ? 'Bitcoin Ordinal' : 'Ethereum ENS';
|
||||
return currentUser?.walletType === 'bitcoin'
|
||||
? 'Bitcoin Ordinal'
|
||||
: 'Ethereum ENS';
|
||||
};
|
||||
|
||||
const getVerificationIcon = () => {
|
||||
@ -123,7 +127,9 @@ export function VerificationStep({
|
||||
};
|
||||
|
||||
const getVerificationColor = () => {
|
||||
return currentUser?.walletType === 'bitcoin' ? 'text-orange-500' : 'text-blue-500';
|
||||
return currentUser?.walletType === 'bitcoin'
|
||||
? 'text-orange-500'
|
||||
: 'text-blue-500';
|
||||
};
|
||||
|
||||
const getVerificationDescription = () => {
|
||||
@ -206,7 +212,9 @@ export function VerificationStep({
|
||||
}
|
||||
|
||||
// Show verification status
|
||||
if (currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED) {
|
||||
if (
|
||||
currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED
|
||||
) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-1 space-y-4">
|
||||
@ -222,8 +230,12 @@ export function VerificationStep({
|
||||
</p>
|
||||
{currentUser && (
|
||||
<div className="text-xs text-neutral-400">
|
||||
{currentUser?.walletType === 'bitcoin' && <p>Ordinal ID: Verified</p>}
|
||||
{currentUser?.walletType === 'ethereum' && <p>ENS Name: Verified</p>}
|
||||
{currentUser?.walletType === 'bitcoin' && (
|
||||
<p>Ordinal ID: Verified</p>
|
||||
)}
|
||||
{currentUser?.walletType === 'ethereum' && (
|
||||
<p>ENS Name: Verified</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Wifi, WifiOff, CheckCircle } from 'lucide-react';
|
||||
import { useNetwork } from '@opchan/react';
|
||||
import { cn } from '../../utils'
|
||||
import { cn } from '../../utils';
|
||||
|
||||
interface WakuHealthIndicatorProps {
|
||||
className?: string;
|
||||
@ -13,7 +13,7 @@ export function WakuHealthIndicator({
|
||||
showText = true,
|
||||
size = 'md',
|
||||
}: WakuHealthIndicatorProps) {
|
||||
const {isConnected, statusMessage} = useNetwork();
|
||||
const { isConnected, statusMessage } = useNetwork();
|
||||
|
||||
const getIcon = () => {
|
||||
if (isConnected === true) {
|
||||
@ -61,7 +61,8 @@ export function WakuHealthIndicator({
|
||||
*/
|
||||
export function WakuHealthDot({ className }: { className?: string }) {
|
||||
const { isConnected } = useNetwork();
|
||||
const statusColor = isConnected === true ? 'green' : isConnected === false ? 'red' : 'gray';
|
||||
const statusColor =
|
||||
isConnected === true ? 'green' : isConnected === false ? 'red' : 'gray';
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -194,7 +194,9 @@ export function WalletConnectionDialog({
|
||||
</p>
|
||||
<p className="text-sm text-neutral-300 mb-2">Address:</p>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
@ -29,8 +29,13 @@ export function WalletWizard({
|
||||
}: WalletWizardProps) {
|
||||
const [currentStep, setCurrentStep] = React.useState<WizardStep>(1);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [delegationStatus, setDelegationStatus] = React.useState<boolean>(false);
|
||||
const { isAuthenticated, verificationStatus, delegationStatus: getDelegationStatus } = useAuth();
|
||||
const [delegationStatus, setDelegationStatus] =
|
||||
React.useState<boolean>(false);
|
||||
const {
|
||||
isAuthenticated,
|
||||
verificationStatus,
|
||||
delegationStatus: getDelegationStatus,
|
||||
} = useAuth();
|
||||
|
||||
// Reset wizard when opened - always start at step 1 for simplicity
|
||||
React.useEffect(() => {
|
||||
@ -43,9 +48,11 @@ export function WalletWizard({
|
||||
// Load delegation status when component mounts or when user changes
|
||||
React.useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
getDelegationStatus().then(status => {
|
||||
setDelegationStatus(status.isValid);
|
||||
}).catch(console.error);
|
||||
getDelegationStatus()
|
||||
.then(status => {
|
||||
setDelegationStatus(status.isValid);
|
||||
})
|
||||
.catch(console.error);
|
||||
} else {
|
||||
setDelegationStatus(false);
|
||||
}
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
|
||||
export {
|
||||
useAuth ,
|
||||
useForum ,
|
||||
useAuth,
|
||||
useForum,
|
||||
useNetwork,
|
||||
usePermissions,
|
||||
useContent,
|
||||
useUIState,
|
||||
useUserDisplay,
|
||||
} from '@opchan/react';
|
||||
|
||||
|
||||
@ -14,7 +14,9 @@ if (!(window as Window & typeof globalThis).Buffer) {
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<WagmiProvider config={config}>
|
||||
<AppKitProvider {...appkitConfig}>
|
||||
<OpchanWithAppKit config={{ ordiscanApiKey: '6bb07766-d98c-4ddd-93fb-6a0e94d629dd' }}>
|
||||
<OpchanWithAppKit
|
||||
config={{ ordiscanApiKey: '6bb07766-d98c-4ddd-93fb-6a0e94d629dd' }}
|
||||
>
|
||||
<App />
|
||||
</OpchanWithAppKit>
|
||||
</AppKitProvider>
|
||||
|
||||
@ -53,8 +53,12 @@ const BookmarksPage = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const postBookmarks = bookmarks.filter(bookmark => bookmark.type === BookmarkType.POST);
|
||||
const commentBookmarks = bookmarks.filter(bookmark => bookmark.type === BookmarkType.COMMENT);
|
||||
const postBookmarks = bookmarks.filter(
|
||||
bookmark => bookmark.type === BookmarkType.POST
|
||||
);
|
||||
const commentBookmarks = bookmarks.filter(
|
||||
bookmark => bookmark.type === BookmarkType.COMMENT
|
||||
);
|
||||
|
||||
const getFilteredBookmarks = () => {
|
||||
switch (activeTab) {
|
||||
@ -79,7 +83,6 @@ const BookmarksPage = () => {
|
||||
await clearAllBookmarks();
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="page-container">
|
||||
<Header />
|
||||
|
||||
@ -20,15 +20,17 @@ const FeedPage: React.FC = () => {
|
||||
const { verificationStatus } = useAuth();
|
||||
const [sortOption, setSortOption] = useState<SortOption>('relevance');
|
||||
|
||||
|
||||
// Build sorted posts from content slices
|
||||
const allPosts = useMemo(() => sortPosts([...content.posts], sortOption), [content.posts, sortOption]);
|
||||
|
||||
// ✅ Get comment count from filtered organized data
|
||||
const getCommentCount = (postId: string) => (content.commentsByPost[postId] || []).length;
|
||||
const allPosts = useMemo(
|
||||
() => sortPosts([...content.posts], sortOption),
|
||||
[content.posts, sortOption]
|
||||
);
|
||||
|
||||
// Loading skeleton
|
||||
if (!content.posts.length && !content.comments.length && !content.cells.length) {
|
||||
if (
|
||||
!content.posts.length &&
|
||||
!content.comments.length &&
|
||||
!content.cells.length
|
||||
) {
|
||||
return (
|
||||
<div className="min-h-screen bg-cyber-dark">
|
||||
<div className="container mx-auto px-4 py-6 max-w-6xl">
|
||||
@ -151,20 +153,13 @@ const FeedPage: React.FC = () => {
|
||||
{verificationStatus !==
|
||||
EVerificationStatus.ENS_ORDINAL_VERIFIED && (
|
||||
<p className="text-sm text-cyber-neutral/80">
|
||||
Connect your wallet to
|
||||
start posting
|
||||
Connect your wallet to start posting
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
allPosts.map(post => (
|
||||
<PostCard
|
||||
key={post.id}
|
||||
post={post}
|
||||
commentCount={getCommentCount(post.id)}
|
||||
/>
|
||||
))
|
||||
allPosts.map(post => <PostCard key={post.id} post={post} />)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useState} from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useForum } from '@opchan/react';
|
||||
import { useAuth } from '@opchan/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@ -27,8 +27,7 @@ import {
|
||||
Globe,
|
||||
Edit3,
|
||||
Save,
|
||||
X,
|
||||
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { EDisplayPreference, EVerificationStatus } from '@opchan/core';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
@ -42,7 +41,6 @@ export default function ProfilePage() {
|
||||
// Get current user from auth context for the address
|
||||
const { currentUser, delegationInfo } = useAuth();
|
||||
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [callSign, setCallSign] = useState('');
|
||||
@ -253,36 +251,31 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
<div className="text-sm text-cyber-neutral">
|
||||
{/* Show ENS name if available */}
|
||||
{(currentUser.ensDetails?.ensName ) && (
|
||||
<div>
|
||||
ENS:{' '}
|
||||
{currentUser.ensDetails?.ensName}
|
||||
</div>
|
||||
{currentUser.ensDetails?.ensName && (
|
||||
<div>ENS: {currentUser.ensDetails?.ensName}</div>
|
||||
)}
|
||||
{/* Show Ordinal details if available */}
|
||||
{(currentUser.ordinalDetails ) && (
|
||||
{currentUser.ordinalDetails && (
|
||||
<div>
|
||||
Ordinal:{' '}
|
||||
{currentUser.ordinalDetails.ordinalDetails}
|
||||
</div>
|
||||
)}
|
||||
{/* Show fallback if neither ENS nor Ordinal */}
|
||||
{!(
|
||||
currentUser.ensDetails?.ensName
|
||||
) &&
|
||||
!(
|
||||
currentUser.ordinalDetails?.ordinalDetails
|
||||
) && <div>No ENS or Ordinal verification</div>}
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
{getVerificationIcon()}
|
||||
<Badge className={getVerificationColor()}>
|
||||
{getVerificationText()}
|
||||
</Badge>
|
||||
{!currentUser.ensDetails?.ensName &&
|
||||
!currentUser.ordinalDetails?.ordinalDetails && (
|
||||
<div>No ENS or Ordinal verification</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
{getVerificationIcon()}
|
||||
<Badge className={getVerificationColor()}>
|
||||
{getVerificationText()}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Wallet Section */}
|
||||
<div className="space-y-3">
|
||||
@ -469,7 +462,9 @@ export default function ProfilePage() {
|
||||
Delegation
|
||||
</span>
|
||||
<Badge
|
||||
variant={delegationInfo.isValid ? 'default' : 'secondary'}
|
||||
variant={
|
||||
delegationInfo.isValid ? 'default' : 'secondary'
|
||||
}
|
||||
className={
|
||||
delegationInfo.isValid
|
||||
? 'bg-green-500/20 text-green-400 border-green-500/30'
|
||||
@ -540,17 +535,17 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
|
||||
{/* Warning for expired delegation */}
|
||||
{(!delegationInfo.isValid && delegationInfo.hasDelegation) && (
|
||||
<div className="p-3 bg-orange-500/10 border border-orange-500/30 rounded-md">
|
||||
<div className="flex items-center gap-2 text-orange-400">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
<span className="text-xs font-medium">
|
||||
Delegation expired. Renew to continue using your
|
||||
browser key.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!delegationInfo.isValid && delegationInfo.hasDelegation && (
|
||||
<div className="p-3 bg-orange-500/10 border border-orange-500/30 rounded-md">
|
||||
<div className="flex items-center gap-2 text-orange-400">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
<span className="text-xs font-medium">
|
||||
Delegation expired. Renew to continue using your
|
||||
browser key.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@ -1,37 +1,57 @@
|
||||
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 { AppKit } from '@reown/appkit';
|
||||
import type { OpChanClientConfig } 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 }) => {
|
||||
const btc = useAppKitAccount({ namespace: 'bip122' });
|
||||
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 => {
|
||||
if (btc.isConnected && btc.address) return { address: btc.address, walletType: 'bitcoin' };
|
||||
if (eth.isConnected && eth.address) return { address: eth.address, walletType: 'ethereum' };
|
||||
if (btc.isConnected && btc.address)
|
||||
return { address: btc.address, walletType: 'bitcoin' };
|
||||
if (eth.isConnected && eth.address)
|
||||
return { address: eth.address, walletType: 'ethereum' };
|
||||
return null;
|
||||
}, [btc.isConnected, btc.address, eth.isConnected, eth.address]);
|
||||
|
||||
const adapter = React.useMemo<WalletAdapter>(() => ({
|
||||
getAccount: () => getCurrent(),
|
||||
onChange: (cb) => {
|
||||
listenersRef.current.add(cb);
|
||||
return () => { listenersRef.current.delete(cb); };
|
||||
},
|
||||
}), [getCurrent]);
|
||||
const adapter = React.useMemo<WalletAdapter>(
|
||||
() => ({
|
||||
getAccount: () => getCurrent(),
|
||||
onChange: cb => {
|
||||
listenersRef.current.add(cb);
|
||||
return () => {
|
||||
listenersRef.current.delete(cb);
|
||||
};
|
||||
},
|
||||
}),
|
||||
[getCurrent]
|
||||
);
|
||||
|
||||
// Notify listeners when AppKit account changes
|
||||
React.useEffect(() => {
|
||||
const account = getCurrent();
|
||||
listenersRef.current.forEach(cb => {
|
||||
try { cb(account); } catch { /* ignore */ }
|
||||
try {
|
||||
cb(account);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
});
|
||||
}, [getCurrent]);
|
||||
|
||||
@ -56,5 +76,3 @@ export const OpchanWithAppKit: React.FC<Props> = ({ config, children }) => {
|
||||
</OpChanProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export { urlLoads } from './urlLoads';
|
||||
export { urlLoads } from './urlLoads';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user