mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-05 07:24:28 +01:00
feat: enable voting in other components
This commit is contained in:
@@ -133,6 +133,7 @@ const voteMutation = extendType({
|
||||
const { item_id, item_type, amount_in_sat } = args;
|
||||
const lightning_address = (await getLightningAddress(item_id, item_type)) ?? BOLT_FUN_LIGHTNING_ADDRESS;
|
||||
const pr = await getPaymetRequestForItem(lightning_address, args.amount_in_sat);
|
||||
console.log(pr);
|
||||
const invoice = parsePaymentRequest({ request: pr });
|
||||
|
||||
// #TODO remove votes rows that get added but not confirmed after some time
|
||||
|
||||
@@ -19,8 +19,8 @@ interface Particle {
|
||||
}
|
||||
|
||||
type Props = {
|
||||
initVotes: number,
|
||||
onVote?: (Vote: number) => void,
|
||||
votes: number,
|
||||
onVote?: (amount: number, config: Partial<{ onSetteled: () => void }>) => void,
|
||||
fillType?: 'leftRight' | 'upDown' | "background" | 'radial',
|
||||
direction?: 'horizontal' | 'vertical'
|
||||
disableCounter?: boolean
|
||||
@@ -30,7 +30,7 @@ type Props = {
|
||||
} & Omit<ComponentProps<typeof Button>, 'children'>
|
||||
|
||||
export default function VoteButton({
|
||||
initVotes,
|
||||
votes,
|
||||
onVote = () => { },
|
||||
fillType = 'leftRight',
|
||||
direction = 'horizontal',
|
||||
@@ -56,9 +56,9 @@ export default function VoteButton({
|
||||
const resetCounterOnRelease = resetCounterOnReleaseProp;
|
||||
|
||||
const doVote = useDebouncedCallback(() => {
|
||||
onVote(voteCntRef.current);
|
||||
const amount = voteCntRef.current;
|
||||
onVote(amount, { onSetteled: () => setVoteCnt(v => v - amount) });
|
||||
voteCntRef.current = 0;
|
||||
console.log("VOTED");
|
||||
|
||||
}, [], 2000)
|
||||
|
||||
@@ -192,7 +192,7 @@ export default function VoteButton({
|
||||
<MdLocalFireDepartment
|
||||
className={`text-body2 ${incrementsCount ? "text-red-600" : "text-red-600"}`}
|
||||
|
||||
/><span className="align-middle w-[4ch]"> {numberFormatter(initVotes + voteCnt)}</span>
|
||||
/><span className="align-middle w-[4ch]"> {numberFormatter(votes + voteCnt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{increments.map(increment => <span
|
||||
|
||||
@@ -61,7 +61,7 @@ export default function BountyCard({ bounty }: Props) {
|
||||
|
||||
<hr className="my-16 bg-gray-200" />
|
||||
<div className="flex gap-24 items-center">
|
||||
<VoteButton initVotes={bounty.votes_count} dense />
|
||||
<VoteButton votes={bounty.votes_count} dense />
|
||||
<div className="text-gray-600">
|
||||
<FiUsers /> <span className="align-middle text-body5">{bounty.applicants_count} Applicants</span>
|
||||
</div>
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function QuestionCard({ question }: Props) {
|
||||
|
||||
<hr className="my-16 bg-gray-200" />
|
||||
<div className="flex gap-24 items-center">
|
||||
<VoteButton initVotes={question.votes_count} dense />
|
||||
<VoteButton votes={question.votes_count} dense />
|
||||
<div className="text-gray-600">
|
||||
<FiUsers /> <span className="align-middle text-body5">{question.answers_count} Answers</span>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import VotesCount from "src/Components/VotesCount/VotesCount"
|
||||
import { Story } from "src/features/Posts/types"
|
||||
import Header from "../Header/Header"
|
||||
import { BiComment } from 'react-icons/bi'
|
||||
import { Link } from "react-router-dom"
|
||||
import VoteButton from "src/Components/VoteButton/VoteButton"
|
||||
import { useVote } from "src/utils/hooks"
|
||||
import { Vote_Item_Type } from 'src/graphql';
|
||||
|
||||
export type StoryCardType = Pick<Story,
|
||||
| 'id'
|
||||
@@ -21,6 +22,12 @@ interface Props {
|
||||
story: StoryCardType
|
||||
}
|
||||
export default function StoryCard({ story }: Props) {
|
||||
|
||||
const { vote } = useVote({
|
||||
itemId: story.id,
|
||||
itemType: Vote_Item_Type.Story
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-12 overflow-hidden border">
|
||||
<img src={story.cover_image} className='h-[200px] w-full object-cover' alt="" />
|
||||
@@ -33,7 +40,7 @@ export default function StoryCard({ story }: Props) {
|
||||
|
||||
<hr className="my-16 bg-gray-200" />
|
||||
<div className="flex gap-24 items-center">
|
||||
<VoteButton initVotes={story.votes_count} dense />
|
||||
<VoteButton votes={story.votes_count} dense onVote={vote} />
|
||||
<div className="text-gray-600">
|
||||
<BiComment /> <span className="align-middle text-body5">{story.comments_count} Comments</span>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function TrendingCard() {
|
||||
return (
|
||||
<div className="bg-white rounded-8 border p-16">
|
||||
<h3 className="text-body2 font-bolder mb-16">Trending on BOLT.FUN</h3>
|
||||
<ul className='flex flex-col gap-16'>
|
||||
<ul className='flex flex-col'>
|
||||
{
|
||||
trendingPosts.loading ?
|
||||
Array(4).fill(0).map((_, idx) => <li key={idx} className="flex items-start gap-8">
|
||||
@@ -24,7 +24,7 @@ export default function TrendingCard() {
|
||||
)
|
||||
:
|
||||
trendingPosts.data?.getTrendingPosts.map(post => {
|
||||
return <Link key={post.id} to={`/blog/post/${post.__typename}/${post.id}`} className="border-b pb-4 last-of-type:border-b-0">
|
||||
return <Link key={post.id} to={`/blog/post/${post.__typename}/${post.id}`} className="border-b py-16 last-of-type:border-b-0">
|
||||
<li className="flex items-start gap-8">
|
||||
<Avatar width={24} src={post.author.avatar} />
|
||||
<p className="text-body5 font-medium">{post.title}</p>
|
||||
|
||||
@@ -43,10 +43,10 @@ export default function FeedPage() {
|
||||
maxHeight: `calc(100vh - ${navHeight}px - 16px)`,
|
||||
overflowY: "scroll",
|
||||
}}>
|
||||
<SortBy
|
||||
{/* <SortBy
|
||||
filterChanged={setSortByFilter}
|
||||
/>
|
||||
<div className="my-24"></div>
|
||||
<div className="my-24"></div> */}
|
||||
<PopularTopicsFilter
|
||||
filterChanged={setTopicFilter}
|
||||
/>
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function PopularTopicsFilter({ filterChanged }: Props) {
|
||||
|
||||
>
|
||||
<span className='bg-gray-50 rounded-8 w-40 h-40 text-center py-8'> </span>
|
||||
<span className="self-center px-16"><Skeleton width={'10ch'} />
|
||||
<span className="self-center px-16"><Skeleton width={'7ch'} />
|
||||
</span>
|
||||
</li>
|
||||
)
|
||||
|
||||
@@ -1,33 +1,62 @@
|
||||
import { BsBookmark } from "react-icons/bs"
|
||||
import { FiArrowLeft } from "react-icons/fi"
|
||||
import { MdIosShare } from "react-icons/md"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import VoteButton from "src/Components/VoteButton/VoteButton"
|
||||
import { Post } from "src/features/Posts/types"
|
||||
import { Vote_Item_Type } from "src/graphql"
|
||||
import { useVote } from "src/utils/hooks"
|
||||
|
||||
interface Props {
|
||||
votes_count: number
|
||||
post: Pick<Post,
|
||||
| 'id'
|
||||
| 'votes_count'
|
||||
| '__typename'
|
||||
>
|
||||
}
|
||||
|
||||
export default function PostActions(props: Props) {
|
||||
export default function PostActions({ post }: Props) {
|
||||
|
||||
const actions = [
|
||||
{
|
||||
icon: BsBookmark,
|
||||
value: 27
|
||||
},
|
||||
{
|
||||
icon: MdIosShare,
|
||||
value: 72
|
||||
value: '--'
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { vote } = useVote({
|
||||
itemId: post.id,
|
||||
itemType: Vote_Item_Type[post.__typename!]
|
||||
});
|
||||
|
||||
return (
|
||||
<ul className="bg-white rounded-12 p-16 border flex justify-around md:flex-col gap-32">
|
||||
<VoteButton initVotes={props.votes_count} direction='vertical' fillType="upDown" />
|
||||
{actions.map((action, idx) => <li
|
||||
className={`py-8 px-20 text-body5 flex flex-col justify-center items-center cursor-pointer rounded-8
|
||||
<div>
|
||||
<button className={`
|
||||
hidden md:flex w-full aspect-square bg-white rounded-12 border justify-around items-center text-gray-500 hover:bg-gray-50 active:bg-gray-100
|
||||
`}
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<FiArrowLeft className={"text-body1"} />
|
||||
</button>
|
||||
{/* <ul className="bg-white rounded-12 p-16 border flex justify-around md:flex-col gap-32">
|
||||
<li
|
||||
className={`py-8 px-20 text-body5 flex flex-col justify-center items-center cursor-pointer rounded-8
|
||||
${'text-gray-500 hover:bg-gray-50 active:bg-gray-100'}`}>
|
||||
<action.icon className={"text-body4 mb-8"}></action.icon>
|
||||
<span>{action.value}</span>
|
||||
</li>)}
|
||||
</ul>
|
||||
<FiArrowLeft className={"text-body4 mb-8"} />
|
||||
</li>
|
||||
</ul> */}
|
||||
<ul className="bg-white rounded-12 p-16 border flex justify-around md:flex-col gap-32 mt-32">
|
||||
{actions.map((action, idx) => <li
|
||||
key={idx}
|
||||
className={`py-8 px-20 text-body5 flex flex-col justify-center items-center cursor-pointer rounded-8
|
||||
${'text-gray-500 hover:bg-gray-50 active:bg-gray-100'}`}>
|
||||
<action.icon className={"text-body4 mb-8"}></action.icon>
|
||||
<span>{action.value}</span>
|
||||
</li>)}
|
||||
<VoteButton votes={post.votes_count} onVote={vote} direction='vertical' fillType="upDown" />
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ export default function PostDetailsPage() {
|
||||
type: type as any
|
||||
},
|
||||
skip: isNaN(Number(id)),
|
||||
|
||||
})
|
||||
|
||||
const { navHeight } = useAppSelector((state) => ({
|
||||
@@ -44,7 +43,7 @@ export default function PostDetailsPage() {
|
||||
top: `${navHeight + 16}px`,
|
||||
maxHeight: `calc(100vh - ${navHeight}px - 16px)`,
|
||||
}}>
|
||||
<PostActions votes_count={post.votes_count} />
|
||||
<PostActions post={post} />
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
||||
@@ -28,16 +28,8 @@ export default function VoteCard({ onClose, direction, projectId, initVotes, ...
|
||||
const [selectedOption, setSelectedOption] = useState(10);
|
||||
const [voteAmount, setVoteAmount] = useState<number>(initVotes ?? 10);
|
||||
const { vote, paymentStatus } = useVote({
|
||||
onSuccess: () => {
|
||||
setTimeout(() => {
|
||||
onClose?.();
|
||||
}, 4000);
|
||||
},
|
||||
onError: () => {
|
||||
setTimeout(() => {
|
||||
onClose?.();
|
||||
}, 4000);
|
||||
}
|
||||
itemId: projectId,
|
||||
itemType: Vote_Item_Type.Project
|
||||
})
|
||||
|
||||
|
||||
@@ -54,7 +46,18 @@ export default function VoteCard({ onClose, direction, projectId, initVotes, ...
|
||||
|
||||
const requestPayment = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
vote({ variables: { "amountInSat": voteAmount, "itemId": projectId!, itemType: Vote_Item_Type.Project } });
|
||||
vote(voteAmount, {
|
||||
onSuccess: () => {
|
||||
setTimeout(() => {
|
||||
onClose?.();
|
||||
}, 4000);
|
||||
},
|
||||
onError: () => {
|
||||
setTimeout(() => {
|
||||
onClose?.();
|
||||
}, 4000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,7 +7,7 @@ $screen-xs-min: 320px;
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
/* background-color: #F8FAFC; */
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import { useState } from 'react';
|
||||
import { useConfirmVoteMutation, useVoteMutation } from 'src/graphql';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useConfirmVoteMutation, useVoteMutation, Vote_Item_Type } from 'src/graphql';
|
||||
import { Wallet_Service } from 'src/services';
|
||||
|
||||
export enum PaymentStatus {
|
||||
@@ -15,80 +15,97 @@ export enum PaymentStatus {
|
||||
}
|
||||
|
||||
|
||||
export const useVote = ({ onSuccess, onError }: {
|
||||
onSuccess?: () => void
|
||||
onError?: (error: any) => void
|
||||
export const useVote = ({ itemId, itemType }: {
|
||||
itemType: Vote_Item_Type,
|
||||
itemId: number
|
||||
}) => {
|
||||
|
||||
const [paymentStatus, setPaymentStatus] = useState<PaymentStatus>(PaymentStatus.DEFAULT);
|
||||
const [voteMutaion] = useVoteMutation();
|
||||
const [confirmVote] = useConfirmVoteMutation();
|
||||
|
||||
const [voteMutaion] = useVoteMutation({
|
||||
onCompleted: async (votingData) => {
|
||||
try {
|
||||
setPaymentStatus(PaymentStatus.AWAITING_PAYMENT);
|
||||
const webln = await Wallet_Service.getWebln()
|
||||
const paymentResponse = await webln.sendPayment(votingData.vote.payment_request);
|
||||
setPaymentStatus(PaymentStatus.PAID);
|
||||
confirmVote({
|
||||
variables: {
|
||||
paymentRequest: votingData.vote.payment_request,
|
||||
preimage: paymentResponse.preimage
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setPaymentStatus(PaymentStatus.NOT_PAID);
|
||||
}
|
||||
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log(error);
|
||||
alert("Something wrong happened...")
|
||||
setPaymentStatus(PaymentStatus.NOT_PAID);
|
||||
onError?.(error)
|
||||
}
|
||||
});
|
||||
|
||||
const [confirmVote] = useConfirmVoteMutation({
|
||||
onCompleted: () => {
|
||||
setPaymentStatus(PaymentStatus.PAYMENT_CONFIRMED);
|
||||
onSuccess?.();
|
||||
},
|
||||
update(cache, { data }) {
|
||||
try {
|
||||
const { item_id, item_type, amount_in_sat } = data!.confirmVote;
|
||||
const { votes_count } = cache.readFragment({
|
||||
id: `${item_type}:${item_id}`,
|
||||
fragment: gql`
|
||||
fragment My${item_type} on ${item_type} {
|
||||
votes_count
|
||||
}`
|
||||
}) ?? {};
|
||||
cache.writeFragment({
|
||||
id: `${item_type}:${item_id}`,
|
||||
fragment: gql`
|
||||
fragment My${item_type} on ${item_type} {
|
||||
votes_count
|
||||
}
|
||||
`,
|
||||
data: {
|
||||
votes_count: votes_count + amount_in_sat
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
onError: () => { }
|
||||
|
||||
});
|
||||
|
||||
|
||||
const vote = (...params: Parameters<typeof voteMutaion>) => {
|
||||
const vote = useCallback((amount: number, config?: Partial<{
|
||||
onSuccess: () => void,
|
||||
onError: (error: any) => void,
|
||||
onSetteled: () => void
|
||||
}>) => {
|
||||
setPaymentStatus(PaymentStatus.FETCHING_PAYMENT_DETAILS)
|
||||
voteMutaion(...params)
|
||||
}
|
||||
voteMutaion({
|
||||
variables: {
|
||||
itemId,
|
||||
itemType,
|
||||
amountInSat: amount
|
||||
},
|
||||
onCompleted: async (votingData) => {
|
||||
try {
|
||||
setPaymentStatus(PaymentStatus.AWAITING_PAYMENT);
|
||||
const webln = await Wallet_Service.getWebln()
|
||||
const paymentResponse = await webln.sendPayment(votingData.vote.payment_request);
|
||||
setPaymentStatus(PaymentStatus.PAID);
|
||||
|
||||
//Confirm Voting payment
|
||||
confirmVote({
|
||||
variables: {
|
||||
paymentRequest: votingData.vote.payment_request,
|
||||
preimage: paymentResponse.preimage
|
||||
},
|
||||
onCompleted: () => {
|
||||
setPaymentStatus(PaymentStatus.PAYMENT_CONFIRMED);
|
||||
config?.onSuccess?.();
|
||||
config?.onSetteled?.()
|
||||
},
|
||||
update(cache, { data }) {
|
||||
try {
|
||||
const { item_id, item_type, amount_in_sat } = data!.confirmVote;
|
||||
const { votes_count } = cache.readFragment({
|
||||
id: `${item_type}:${item_id}`,
|
||||
fragment: gql`
|
||||
fragment My${item_type} on ${item_type} {
|
||||
votes_count
|
||||
}`
|
||||
}) ?? {};
|
||||
cache.writeFragment({
|
||||
id: `${item_type}:${item_id}`,
|
||||
fragment: gql`
|
||||
fragment My${item_type} on ${item_type} {
|
||||
votes_count
|
||||
}
|
||||
`,
|
||||
data: {
|
||||
votes_count: votes_count + amount_in_sat
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
config?.onError?.(error)
|
||||
}
|
||||
},
|
||||
|
||||
onError: (error) => {
|
||||
config?.onError?.(error);
|
||||
config?.onSetteled?.();
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
setPaymentStatus(PaymentStatus.CANCELED);
|
||||
config?.onError?.(error);
|
||||
config?.onSetteled?.();
|
||||
alert("Payment rejected by user")
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log(error);
|
||||
alert("Something wrong happened...")
|
||||
setPaymentStatus(PaymentStatus.NOT_PAID);
|
||||
config?.onError?.(error);
|
||||
config?.onSetteled?.();
|
||||
}
|
||||
})
|
||||
}, [confirmVote, itemId, itemType, voteMutaion]);
|
||||
|
||||
|
||||
|
||||
return {
|
||||
paymentStatus,
|
||||
vote
|
||||
|
||||
Reference in New Issue
Block a user