mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-02-20 22:14:40 +01:00
feat: comments nostr tooltip, relays connection status, wrap nav content in page-container, show total comments count on story card, created prefernces service
This commit is contained in:
@@ -377,6 +377,7 @@ export interface NexusGenFieldTypes {
|
||||
author: NexusGenRootTypes['Author']; // Author!
|
||||
body: string; // String!
|
||||
comments: NexusGenRootTypes['PostComment'][]; // [PostComment!]!
|
||||
comments_count: number; // Int!
|
||||
cover_image: string | null; // String
|
||||
createdAt: NexusGenScalars['Date']; // Date!
|
||||
excerpt: string; // String!
|
||||
@@ -585,6 +586,7 @@ export interface NexusGenFieldTypeNames {
|
||||
author: 'Author'
|
||||
body: 'String'
|
||||
comments: 'PostComment'
|
||||
comments_count: 'Int'
|
||||
cover_image: 'String'
|
||||
createdAt: 'Date'
|
||||
excerpt: 'String'
|
||||
|
||||
@@ -187,6 +187,7 @@ type Story implements PostBase {
|
||||
author: Author!
|
||||
body: String!
|
||||
comments: [PostComment!]!
|
||||
comments_count: Int!
|
||||
cover_image: String
|
||||
createdAt: Date!
|
||||
excerpt: String!
|
||||
|
||||
@@ -80,21 +80,21 @@ const Story = objectType({
|
||||
type: "Tag",
|
||||
resolve: (parent) => prisma.story.findUnique({ where: { id: parent.id } }).tags()
|
||||
});
|
||||
// t.nonNull.int('comments_count', {
|
||||
// resolve: async (parent) => {
|
||||
// const post = await prisma.story.findUnique({
|
||||
// where: { id: parent.id },
|
||||
// include: {
|
||||
// _count: {
|
||||
// select: {
|
||||
// comments: true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// return post._count.comments;
|
||||
// }
|
||||
// });
|
||||
t.nonNull.int('comments_count', {
|
||||
resolve: async (parent) => {
|
||||
const post = await prisma.story.findUnique({
|
||||
where: { id: parent.id },
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
comments: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return post._count.comments;
|
||||
}
|
||||
});
|
||||
t.nonNull.field('author', {
|
||||
type: "Author",
|
||||
resolve: (parent) =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { UnionToObjectKeys } from 'src/utils/types/utils';
|
||||
import { Link } from 'react-router-dom'
|
||||
import { FallingLines, LineWave, TailSpin } from 'react-loader-spinner';
|
||||
import { TailSpin } from 'react-loader-spinner';
|
||||
|
||||
type Props = {
|
||||
color?: 'primary' | 'red' | 'white' | 'gray' | "black" | 'none',
|
||||
@@ -107,8 +107,10 @@ const Button = React.forwardRef<any, Props>(({ color = 'white',
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{isLoading && <div className="text-body5 absolute inset-1 bg-inherit flex flex-col justify-center items-center">
|
||||
<span className={isLoading ? "opacity-0" : ""}>
|
||||
{children}
|
||||
</span>
|
||||
{isLoading && <div className="text-body5 absolute inset-0 rounded-lg bg-inherit flex flex-col justify-center items-center">
|
||||
{loadingText ?? <TailSpin
|
||||
width="24"
|
||||
color={loadingColor[color]}
|
||||
|
||||
@@ -40,113 +40,114 @@ export default function NavDesktop() {
|
||||
|
||||
|
||||
return (
|
||||
<nav className="bg-white flex py-16 px-32 items-center w-full min-w-full">
|
||||
<Link to="/">
|
||||
<h2 className="text-h5 font-bold mr-40 lg:mr-64">
|
||||
<img className='h-40' src={ASSETS.Logo} alt="Bolt fun logo" />
|
||||
</h2>
|
||||
</Link>
|
||||
<ul className="flex gap-32 xl:gap-64">
|
||||
<li className="relative">
|
||||
<Link to={'/products'} className='text-body4 font-bold hover:text-primary-600'>
|
||||
Products
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Menu
|
||||
offsetY={28}
|
||||
menuButton={
|
||||
<MenuButton
|
||||
className='text-body4 font-bold hover:text-primary-600'>Community <FiChevronDown className="ml-8" />
|
||||
</MenuButton>
|
||||
}
|
||||
menuClassName='!rounded-12 !p-8 !border-gray-200'
|
||||
menuStyle={{ border: '1px solid' }}
|
||||
>
|
||||
<MenuItem
|
||||
href="/blog"
|
||||
onClick={(e) => {
|
||||
e.syntheticEvent.preventDefault();
|
||||
navigate("/blog");
|
||||
}}
|
||||
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12 '
|
||||
<nav className="bg-white py-16 flex items-center w-full min-w-full">
|
||||
<div className="page-container flex items-center !p-0">
|
||||
<Link to="/">
|
||||
<h2 className="text-h5 font-bold mr-40 lg:mr-64">
|
||||
<img className='h-40' src={ASSETS.Logo} alt="Bolt fun logo" />
|
||||
</h2>
|
||||
</Link>
|
||||
<ul className="flex gap-32 xl:gap-64">
|
||||
<li className="relative">
|
||||
<Link to={'/products'} className='text-body4 font-bold hover:text-primary-600'>
|
||||
Products
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Menu
|
||||
offsetY={28}
|
||||
menuButton={
|
||||
<MenuButton
|
||||
className='text-body4 font-bold hover:text-primary-600'>Community <FiChevronDown className="ml-8" />
|
||||
</MenuButton>
|
||||
}
|
||||
menuClassName='!rounded-12 !p-8 !border-gray-200'
|
||||
menuStyle={{ border: '1px solid' }}
|
||||
>
|
||||
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
|
||||
{/* <FiFeather className={`text-body1 inline-block text-primary-600 `} /> */}
|
||||
<span className="text-body2">✍🏼</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-body4 text-black font-medium">
|
||||
Stories
|
||||
</p>
|
||||
<p className="text-body5 text-gray-600 mt-4">
|
||||
Tales from the maker community
|
||||
</p>
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
<MenuItem
|
||||
href="/blog"
|
||||
onClick={(e) => {
|
||||
e.syntheticEvent.preventDefault();
|
||||
navigate("/blog");
|
||||
}}
|
||||
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12 '
|
||||
>
|
||||
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
|
||||
{/* <FiFeather className={`text-body1 inline-block text-primary-600 `} /> */}
|
||||
<span className="text-body2">✍🏼</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-body4 text-black font-medium">
|
||||
Stories
|
||||
</p>
|
||||
<p className="text-body5 text-gray-600 mt-4">
|
||||
Tales from the maker community
|
||||
</p>
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
|
||||
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12 opacity-40'
|
||||
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12 opacity-40'
|
||||
>
|
||||
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
|
||||
<span className="text-body2">💬</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-body4 text-black font-medium">
|
||||
Discussions
|
||||
</p>
|
||||
<p className="text-body5 text-gray-600 mt-4">
|
||||
Coming soon
|
||||
</p>
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
href="/hackathons"
|
||||
onClick={(e) => {
|
||||
e.syntheticEvent.preventDefault();
|
||||
navigate("/hackathons");
|
||||
}}
|
||||
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
|
||||
>
|
||||
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
|
||||
<span className="text-body2">🏆</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-body4 text-black font-medium">
|
||||
Hackathons
|
||||
</p>
|
||||
<p className="text-body5 text-gray-600 mt-4">
|
||||
Take part in hackathons & tournaments
|
||||
</p>
|
||||
</div>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</li>
|
||||
<li className="relative">
|
||||
<a
|
||||
href={'https://bolt.fun/guide/'}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className='text-body4 font-bold hover:text-primary-600'
|
||||
>
|
||||
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
|
||||
<span className="text-body2">💬</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-body4 text-black font-medium">
|
||||
Discussions
|
||||
</p>
|
||||
<p className="text-body5 text-gray-600 mt-4">
|
||||
Coming soon
|
||||
</p>
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
href="/hackathons"
|
||||
onClick={(e) => {
|
||||
e.syntheticEvent.preventDefault();
|
||||
navigate("/hackathons");
|
||||
}}
|
||||
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
|
||||
>
|
||||
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
|
||||
<span className="text-body2">🏆</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-body4 text-black font-medium">
|
||||
Hackathons
|
||||
</p>
|
||||
<p className="text-body5 text-gray-600 mt-4">
|
||||
Take part in hackathons & tournaments
|
||||
</p>
|
||||
</div>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</li>
|
||||
<li className="relative">
|
||||
<a
|
||||
href={'https://bolt.fun/guide/'}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className='text-body4 font-bold hover:text-primary-600'
|
||||
>
|
||||
Guide
|
||||
</a>
|
||||
</li>
|
||||
<li className="relative">
|
||||
<Link to={'/donate'} className='text-body4 font-bold hover:text-primary-600'>
|
||||
Donate
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
Guide
|
||||
</a>
|
||||
</li>
|
||||
<li className="relative">
|
||||
<Link to={'/donate'} className='text-body4 font-bold hover:text-primary-600'>
|
||||
Donate
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="flex-1"></div>
|
||||
<div className="flex-1"></div>
|
||||
|
||||
<motion.div
|
||||
animate={searchOpen ? { opacity: 0 } : { opacity: 1 }}
|
||||
className="flex"
|
||||
>
|
||||
<motion.div
|
||||
animate={searchOpen ? { opacity: 0 } : { opacity: 1 }}
|
||||
className="flex"
|
||||
>
|
||||
|
||||
{/* <Button
|
||||
{/* <Button
|
||||
color="primary"
|
||||
size="md"
|
||||
className="lg:px-40"
|
||||
@@ -155,75 +156,76 @@ export default function NavDesktop() {
|
||||
>
|
||||
Submit App️
|
||||
</Button> */}
|
||||
{/* {isWalletConnected ?
|
||||
{/* {isWalletConnected ?
|
||||
<Button className="ml-16 py-12 px-16 lg:px-20">Connected <AiFillThunderbolt className='inline-block text-thunder transform scale-125' /></Button>
|
||||
: <Button className="ml-16 py-12 px-16 lg:px-20" onClick={onConnectWallet}><AiFillThunderbolt className='inline-block text-thunder transform scale-125' /> Connect Wallet </Button>
|
||||
} */}
|
||||
|
||||
{currentSection === 'products' && <IconButton className='mr-16 self-center' onClick={openSearch}>
|
||||
<BsSearch className='scale-125 text-gray-400' />
|
||||
</IconButton>}
|
||||
</motion.div>
|
||||
{curUser !== undefined &&
|
||||
(curUser ?
|
||||
<Menu
|
||||
menuClassName='!p-8 !rounded-12'
|
||||
menuButton={<MenuButton ><Avatar src={curUser.avatar} width={40} /> </MenuButton>}>
|
||||
<MenuItem
|
||||
href={createRoute({ type: 'profile', id: curUser.id, username: curUser.name })}
|
||||
onClick={(e) => {
|
||||
e.syntheticEvent.preventDefault();
|
||||
navigate(createRoute({ type: 'profile', id: curUser.id, username: curUser.name }));
|
||||
}}
|
||||
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
|
||||
>
|
||||
Profile
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
href="/logout"
|
||||
onClick={(e) => {
|
||||
e.syntheticEvent.preventDefault();
|
||||
navigate("/logout");
|
||||
}}
|
||||
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
|
||||
>
|
||||
Logout
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
:
|
||||
<Button color="primary" href="/login">
|
||||
Connect ⚡
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<div className="relative h-36">
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: '0'
|
||||
}}
|
||||
animate={searchOpen ? {
|
||||
opacity: 1,
|
||||
y: '0',
|
||||
transition: { type: "spring", stiffness: 70 }
|
||||
|
||||
} : {
|
||||
opacity: 0,
|
||||
y: '-120px',
|
||||
transition: {
|
||||
ease: "easeIn"
|
||||
}
|
||||
}}
|
||||
className='absolute top-0 right-0 flex items-center h-full'
|
||||
>
|
||||
<Search
|
||||
width={326}
|
||||
isOpen={searchOpen}
|
||||
onClose={() => setSearchOpen(false)}
|
||||
onResultClick={() => setSearchOpen(false)}
|
||||
/>
|
||||
{currentSection === 'products' && <IconButton className='mr-16 self-center' onClick={openSearch}>
|
||||
<BsSearch className='scale-125 text-gray-400' />
|
||||
</IconButton>}
|
||||
</motion.div>
|
||||
{curUser !== undefined &&
|
||||
(curUser ?
|
||||
<Menu
|
||||
menuClassName='!p-8 !rounded-12'
|
||||
menuButton={<MenuButton ><Avatar src={curUser.avatar} width={40} /> </MenuButton>}>
|
||||
<MenuItem
|
||||
href={createRoute({ type: 'profile', id: curUser.id, username: curUser.name })}
|
||||
onClick={(e) => {
|
||||
e.syntheticEvent.preventDefault();
|
||||
navigate(createRoute({ type: 'profile', id: curUser.id, username: curUser.name }));
|
||||
}}
|
||||
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
|
||||
>
|
||||
Profile
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
href="/logout"
|
||||
onClick={(e) => {
|
||||
e.syntheticEvent.preventDefault();
|
||||
navigate("/logout");
|
||||
}}
|
||||
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
|
||||
>
|
||||
Logout
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
:
|
||||
<Button color="primary" href="/login">
|
||||
Connect ⚡
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<div className="relative h-36">
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: '0'
|
||||
}}
|
||||
animate={searchOpen ? {
|
||||
opacity: 1,
|
||||
y: '0',
|
||||
transition: { type: "spring", stiffness: 70 }
|
||||
|
||||
} : {
|
||||
opacity: 0,
|
||||
y: '-120px',
|
||||
transition: {
|
||||
ease: "easeIn"
|
||||
}
|
||||
}}
|
||||
className='absolute top-0 right-0 flex items-center h-full'
|
||||
>
|
||||
<Search
|
||||
width={326}
|
||||
isOpen={searchOpen}
|
||||
onClose={() => setSearchOpen(false)}
|
||||
onResultClick={() => setSearchOpen(false)}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
|
||||
@@ -74,45 +74,47 @@ export default function NavMobile() {
|
||||
|
||||
return (
|
||||
<div className={`${styles.navMobile}`}>
|
||||
<nav className={`bg-white h-[67px] w-full p-16 px-32 flex justify-between items-center`}>
|
||||
<Link to="/">
|
||||
<img className='h-32' src={ASSETS.Logo} alt="Bolt fun logo" />
|
||||
</Link>
|
||||
<nav className={`bg-white h-[67px] w-full py-16`}>
|
||||
<div className="page-container flex justify-between items-center !p-0">
|
||||
<Link to="/">
|
||||
<img className='h-32' src={ASSETS.Logo} alt="Bolt fun logo" />
|
||||
</Link>
|
||||
|
||||
<div className="ml-auto"></div>
|
||||
{curUser !== undefined &&
|
||||
(curUser &&
|
||||
<Menu
|
||||
menuClassName='!p-8 !rounded-12'
|
||||
menuButton={<MenuButton ><Avatar src={curUser.avatar} width={32} /> </MenuButton>}>
|
||||
<MenuItem
|
||||
href={createRoute({ type: 'profile', id: curUser.id, username: curUser.name })}
|
||||
onClick={(e) => {
|
||||
e.syntheticEvent.preventDefault();
|
||||
navigate(createRoute({ type: 'profile', id: curUser.id, username: curUser.name }));
|
||||
}}
|
||||
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
|
||||
>
|
||||
Profile
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
href="/logout"
|
||||
onClick={(e) => {
|
||||
e.syntheticEvent.preventDefault();
|
||||
navigate("/logout");
|
||||
}}
|
||||
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
|
||||
>
|
||||
Logout
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<div className="ml-auto"></div>
|
||||
{curUser !== undefined &&
|
||||
(curUser &&
|
||||
<Menu
|
||||
menuClassName='!p-8 !rounded-12'
|
||||
menuButton={<MenuButton ><Avatar src={curUser.avatar} width={32} /> </MenuButton>}>
|
||||
<MenuItem
|
||||
href={createRoute({ type: 'profile', id: curUser.id, username: curUser.name })}
|
||||
onClick={(e) => {
|
||||
e.syntheticEvent.preventDefault();
|
||||
navigate(createRoute({ type: 'profile', id: curUser.id, username: curUser.name }));
|
||||
}}
|
||||
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
|
||||
>
|
||||
Profile
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
href="/logout"
|
||||
onClick={(e) => {
|
||||
e.syntheticEvent.preventDefault();
|
||||
navigate("/logout");
|
||||
}}
|
||||
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
|
||||
>
|
||||
Logout
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
)
|
||||
}
|
||||
<IconButton className='auto text-2xl w-[50px] h-[50px] hover:bg-gray-200 self-center' onClick={() => toggleDrawerOpen()}>
|
||||
{!drawerOpen ? (<motion.div key={drawerOpen ? 1 : 0} variants={navBtnVariant} initial='menuHide' animate='menuShow'><FiMenu /></motion.div>)
|
||||
: (<motion.div key={drawerOpen ? 1 : 0} variants={navBtnVariant} initial='closeHide' animate='closeShow'><GrClose /></motion.div>)}
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
<IconButton className='auto text-2xl w-[50px] h-[50px] hover:bg-gray-200 self-center' onClick={() => toggleDrawerOpen()}>
|
||||
{!drawerOpen ? (<motion.div key={drawerOpen ? 1 : 0} variants={navBtnVariant} initial='menuHide' animate='menuShow'><FiMenu /></motion.div>)
|
||||
: (<motion.div key={drawerOpen ? 1 : 0} variants={navBtnVariant} initial='closeHide' animate='closeShow'><GrClose /></motion.div>)}
|
||||
</IconButton>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="fixed left-0 top-[67px] pointer-events-none z-[2010] w-full min-h-[calc(100vh-67px)]">
|
||||
|
||||
@@ -33,6 +33,7 @@ export default function Comment({ comment, canReply, isRoot, onClickedReply, onR
|
||||
const handleReply = async (text: string) => {
|
||||
try {
|
||||
await onReply?.(text);
|
||||
toggleRepliesCollapsed(false);
|
||||
setReplyOpen(false);
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -58,7 +59,7 @@ export default function Comment({ comment, canReply, isRoot, onClickedReply, onR
|
||||
key={reply.id}
|
||||
comment={reply}
|
||||
onClickedReply={clickReply}
|
||||
canReply={false}
|
||||
canReply={!!isRoot}
|
||||
/>)}
|
||||
{replyOpen && <AddComment
|
||||
avatar={user?.avatar!}
|
||||
|
||||
@@ -8,6 +8,11 @@ import { useAppSelector } from "src/utils/hooks";
|
||||
import * as CommentsWorker from './comments.worker'
|
||||
import { Post_Type } from 'src/graphql'
|
||||
import useComments from './useComments'
|
||||
import IconButton from 'src/Components/IconButton/IconButton'
|
||||
import { AiOutlineClose } from 'react-icons/ai'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { createRoute } from 'src/utils/routing'
|
||||
import Preferences from 'src/services/preferences.service'
|
||||
|
||||
// const createWorker = createWorkerFactory(() => import('./comments.worker'));
|
||||
|
||||
@@ -23,7 +28,8 @@ export default function CommentsSection({ type, id }: Props) {
|
||||
// const commentsTree = useMemo(() => convertCommentsToTree(comments), [comments])
|
||||
|
||||
// const [commentsTree, setCommentsTree] = useState<Comment[]>([])
|
||||
const user = useAppSelector(state => state.user.me)
|
||||
const user = useAppSelector(state => state.user.me);
|
||||
const [showTooltip, setShowTooltip] = useState(Preferences.get('showNostrCommentsTooltip'))
|
||||
// const filter = useMemo(() => `boltfun ${type}_comment ${id}` + (process.env.NODE_ENV === 'development' ? ' dev' : ""), [id, type])
|
||||
|
||||
// useEffect(() => {
|
||||
@@ -36,7 +42,7 @@ export default function CommentsSection({ type, id }: Props) {
|
||||
// unsub();
|
||||
// }
|
||||
// }, [filter]);
|
||||
const { commentsTree, postComment } = useComments({ type, id })
|
||||
const { commentsTree, postComment, connectionStatus } = useComments({ type, id })
|
||||
|
||||
const handleNewComment = async (content: string, parentId?: string) => {
|
||||
try {
|
||||
@@ -47,10 +53,25 @@ export default function CommentsSection({ type, id }: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
const closeTooltip = () => {
|
||||
Preferences.update('showNostrCommentsTooltip', false);
|
||||
setShowTooltip(false);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="border-2 border-gray-200 rounded-12 md:rounded-16 p-32 bg-white">
|
||||
<h6 className="text-body2 font-bolder">Discussion</h6>
|
||||
<div className="flex flex-wrap justify-between">
|
||||
<h6 className="text-body2 font-bolder">Discussion</h6>
|
||||
{connectionStatus.status === 'Connected' && <div className="bg-green-50 text-green-500 text-body5 font-medium py-4 px-12 rounded-48"> • Connected to {connectionStatus.connectedRelaysCount} relays 📡</div>}
|
||||
{connectionStatus.status === 'Connecting' && <div className="bg-amber-50 text-amber-500 text-body5 font-medium py-4 px-12 rounded-48"> • Connecting to relays ⌛</div>}
|
||||
{connectionStatus.status === 'Not Connected' && <div className="bg-red-50 text-red-500 text-body5 font-medium py-4 px-12 rounded-48"> • Not connected 📡</div>}
|
||||
</div>
|
||||
{showTooltip && <div className="bg-gray-900 text-white p-16 rounded-12 my-24 flex items-center justify-between gap-12">
|
||||
<span>💬</span>
|
||||
<p className="text-body4 font-medium">Learn about <Link to={createRoute({ type: "story", title: "What is Nostr", id: 999 })} className='underline'>how your data is stored</Link> with Nostr comments and relays</p>
|
||||
<IconButton className='shrink-0 self-start' onClick={closeTooltip}><AiOutlineClose className='text-gray-600' /></IconButton>
|
||||
</div>}
|
||||
{!!user && <div className="mt-24">
|
||||
<AddComment
|
||||
placeholder='Leave a comment...'
|
||||
|
||||
@@ -16,6 +16,7 @@ const useComments = (config: {
|
||||
const commentsEventsTemp = useRef<Record<string, Required<NostrEvent>>>({})
|
||||
const [commentsEvents, setCommentsEvents] = useDebouncedState<Record<string, Required<NostrEvent>>>({}, 1000)
|
||||
const pendingResolvers = useRef<Record<string, () => void>>({});
|
||||
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>({ status: "Connecting", connectedRelaysCount: 0 })
|
||||
const filter = useMemo(() => `boltfun ${config.type}_comment ${config.id}` + (process.env.NODE_ENV === 'development' ? ' dev' : ""), [config.id, config.type])
|
||||
|
||||
const [commentsTree, setCommentsTree] = useState<Comment[]>([])
|
||||
@@ -55,7 +56,21 @@ const useComments = (config: {
|
||||
}
|
||||
});
|
||||
})();
|
||||
}, [commentsEvents])
|
||||
}, [commentsEvents]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
const newStatus = getConnectionStatus();
|
||||
if (newStatus.connectedRelaysCount !== connectionStatus.connectedRelaysCount || newStatus.status !== connectionStatus.status)
|
||||
setConnectionStatus(newStatus);
|
||||
}, 5000)
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [connectionStatus.connectedRelaysCount, connectionStatus.status])
|
||||
|
||||
|
||||
const postComment = useCallback(async ({ content, parentId }: {
|
||||
content: string,
|
||||
@@ -110,7 +125,7 @@ const useComments = (config: {
|
||||
}, [filter]);
|
||||
|
||||
|
||||
return { commentsTree, postComment }
|
||||
return { commentsTree, postComment, connectionStatus }
|
||||
}
|
||||
|
||||
export default useComments;
|
||||
@@ -249,4 +264,30 @@ async function buildTree(events: Record<string, Required<NostrEvent>>) {
|
||||
const sortedTree = Object.values(eventsTree).sort((a, b) => b.created_at - a.created_at)
|
||||
|
||||
return sortedTree;
|
||||
}
|
||||
|
||||
type ConnectionStatus = {
|
||||
status: 'Connected' | "Connecting" | "Not Connected",
|
||||
connectedRelaysCount: number
|
||||
}
|
||||
|
||||
function getConnectionStatus(): ConnectionStatus {
|
||||
let openedCnt = 0, reconnectingCnt = 0;
|
||||
|
||||
for (const relayUrl in pool.relays) {
|
||||
const relayStatus = pool.relays[relayUrl].relay.status;
|
||||
if (relayStatus === 1) openedCnt += 1;
|
||||
if (relayStatus === 0) reconnectingCnt += 1;
|
||||
}
|
||||
|
||||
const finalStatus = openedCnt > 0 ?
|
||||
"Connected" :
|
||||
reconnectingCnt > 0 ?
|
||||
"Connecting" :
|
||||
"Not Connected";
|
||||
|
||||
return {
|
||||
status: finalStatus,
|
||||
connectedRelaysCount: openedCnt
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { useVote } from "src/utils/hooks"
|
||||
import { Author, Tag, Vote_Item_Type } from 'src/graphql';
|
||||
import Badge from "src/Components/Badge/Badge"
|
||||
import { createRoute } from "src/utils/routing"
|
||||
import { BiComment } from "react-icons/bi"
|
||||
|
||||
|
||||
export type StoryCardType = Pick<Story,
|
||||
@@ -16,6 +17,7 @@ export type StoryCardType = Pick<Story,
|
||||
| 'createdAt'
|
||||
| 'excerpt'
|
||||
| 'votes_count'
|
||||
| 'comments_count'
|
||||
> & {
|
||||
tags: Array<Pick<Tag, 'id' | "title">>,
|
||||
author: Pick<Author, 'id' | 'name' | 'avatar' | 'join_date'>
|
||||
@@ -49,9 +51,9 @@ export default function StoryCard({ story }: Props) {
|
||||
<hr className="my-16 bg-gray-200" />
|
||||
<div className="flex gap-24 items-center">
|
||||
<VoteButton votes={story.votes_count} dense onVote={vote} />
|
||||
{/* <div className="text-gray-600">
|
||||
<div className="text-gray-600">
|
||||
<BiComment /> <span className="align-middle text-body5">{story.comments_count} Comments</span>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@ query Feed($take: Int, $skip: Int, $sortBy: String, $tag: Int) {
|
||||
votes_count
|
||||
type
|
||||
cover_image
|
||||
# comments_count
|
||||
comments_count
|
||||
}
|
||||
... on Bounty {
|
||||
id
|
||||
|
||||
@@ -330,6 +330,7 @@ export type Story = PostBase & {
|
||||
author: Author;
|
||||
body: Scalars['String'];
|
||||
comments: Array<PostComment>;
|
||||
comments_count: Scalars['Int'];
|
||||
cover_image: Maybe<Scalars['String']>;
|
||||
createdAt: Scalars['Date'];
|
||||
excerpt: Scalars['String'];
|
||||
@@ -504,7 +505,7 @@ export type FeedQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type FeedQuery = { __typename?: 'Query', getFeed: Array<{ __typename?: 'Bounty', id: number, title: string, createdAt: any, excerpt: string, votes_count: number, type: string, cover_image: string | null, deadline: string, reward_amount: number, applicants_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | { __typename?: 'Question', id: number, title: string, createdAt: any, excerpt: string, votes_count: number, type: string, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | { __typename?: 'Story', id: number, title: string, createdAt: any, excerpt: string, votes_count: number, type: string, cover_image: string | null, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> }> };
|
||||
export type FeedQuery = { __typename?: 'Query', getFeed: Array<{ __typename?: 'Bounty', id: number, title: string, createdAt: any, excerpt: string, votes_count: number, type: string, cover_image: string | null, deadline: string, reward_amount: number, applicants_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | { __typename?: 'Question', id: number, title: string, createdAt: any, excerpt: string, votes_count: number, type: string, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | { __typename?: 'Story', id: number, title: string, createdAt: any, excerpt: string, votes_count: number, type: string, cover_image: string | null, comments_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> }> };
|
||||
|
||||
export type PostDetailsQueryVariables = Exact<{
|
||||
id: Scalars['Int'];
|
||||
@@ -1124,6 +1125,7 @@ export const FeedDocument = gql`
|
||||
votes_count
|
||||
type
|
||||
cover_image
|
||||
comments_count
|
||||
}
|
||||
... on Bounty {
|
||||
id
|
||||
|
||||
40
src/services/preferences.service.ts
Normal file
40
src/services/preferences.service.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
|
||||
type PreferencesType = {
|
||||
showNostrCommentsTooltip: boolean;
|
||||
themeMode: 'light' | 'dark'
|
||||
|
||||
}
|
||||
|
||||
const defaultPrefernces: PreferencesType = {
|
||||
showNostrCommentsTooltip: true,
|
||||
themeMode: 'light'
|
||||
}
|
||||
|
||||
export default class Preferences {
|
||||
|
||||
|
||||
private static preferencesObject: PreferencesType;
|
||||
|
||||
|
||||
static init() {
|
||||
const str = localStorage.getItem('preferences');
|
||||
if (!str)
|
||||
this.preferencesObject = defaultPrefernces;
|
||||
else
|
||||
this.preferencesObject = JSON.parse(str)
|
||||
}
|
||||
|
||||
static update<T extends keyof PreferencesType>(key: T, value: PreferencesType[T]) {
|
||||
if (!this.preferencesObject)
|
||||
this.init();
|
||||
this.preferencesObject[key] = value;
|
||||
localStorage.setItem('preferences', JSON.stringify(this.preferencesObject))
|
||||
}
|
||||
|
||||
static get<T extends keyof PreferencesType>(key: T): PreferencesType[T] {
|
||||
if (!this.preferencesObject)
|
||||
this.init();
|
||||
return this.preferencesObject[key];
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
|
||||
const DEFAULT_RELAYS = [
|
||||
'wss://nostr.drss.io',
|
||||
'wss://nostr-relay.freeberty.net',
|
||||
'wss://nostr.unknown.place',
|
||||
'wss://nostr-relay.untethr.me',
|
||||
'wss://relay.damus.io'
|
||||
'wss://relay.damus.io',
|
||||
];
|
||||
|
||||
const CONSTS = {
|
||||
|
||||
Reference in New Issue
Block a user