feat: re-add "donate" and add "get annual membership" buttons to project page

This commit is contained in:
Artur N
2024-08-12 20:03:27 -03:00
parent b533f48088
commit f164fa8858
13 changed files with 136 additions and 211 deletions

View File

@@ -32,10 +32,7 @@ const DonationFormModal: React.FC<Props> = ({ project }) => {
.object({
name: z.string().optional(),
email: z.string().email().optional(),
amount: z.coerce
.number()
.min(1)
.max(MAX_AMOUNT / 100),
amount: z.coerce.number().min(1).max(MAX_AMOUNT),
taxDeductible: z.enum(['yes', 'no']),
})
.refine((data) => (!isAuthed && data.taxDeductible === 'yes' ? !!data.name : true), {

View File

@@ -31,10 +31,7 @@ const MembershipFormModal: React.FC<Props> = ({ project }) => {
.object({
name: z.string().optional(),
email: z.string().email().optional(),
amount: z.coerce
.number()
.min(1)
.max(MAX_AMOUNT / 100),
amount: z.coerce.number().min(1).max(MAX_AMOUNT),
taxDeductible: z.enum(['yes', 'no']),
recurring: z.enum(['yes', 'no']),
})

View File

@@ -1,7 +1,5 @@
export const CURRENCY = 'usd'
// Set your amount limits: Use float for decimal currencies and
// Integer for zero-decimal currencies: https://stripe.com/docs/currencies#zero-decimal.
export const MIN_AMOUNT = 100
export const MAX_AMOUNT = 500000
export const AMOUNT_STEP = 500
export const MEMBERSHIP_PRICE = 10000
export const MIN_AMOUNT = 1
export const MAX_AMOUNT = 500
export const AMOUNT_STEP = 5
export const MEMBERSHIP_PRICE = 100

View File

@@ -48,7 +48,7 @@ function MyDonations() {
<TableCell>{donation.projectName}</TableCell>
<TableCell>{donation.fund}</TableCell>
<TableCell>{donation.btcPayInvoiceId ? 'Crypto' : 'Fiat'}</TableCell>
<TableCell>${donation.fiatAmount / 100}</TableCell>
<TableCell>${donation.fiatAmount}</TableCell>
<TableCell>{dayjs(donation.createdAt).format('lll')}</TableCell>
</TableRow>
))}

View File

@@ -64,9 +64,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
`stores/${env.BTCPAY_STORE_ID}/invoices/${body.invoiceId}/payment-methods`
)
const fiatAmount = Math.round(
Number(paymentMethods[0].amount) * Number(paymentMethods[0].rate) * 100
)
const cryptoAmount = Number(paymentMethods[0].amount)
const fiatAmount = Number(paymentMethods[0].amount) * Number(paymentMethods[0].rate)
await prisma.donation.create({
data: {
@@ -76,6 +75,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
projectSlug: body.metadata.projectSlug,
fund: 'Monero Fund',
cryptoCode: paymentMethods[0].cryptoCode,
cryptoAmount,
fiatAmount,
membershipExpiresAt:
body.metadata.isMembership === 'true' ? dayjs().add(1, 'year').toDate() : null,

View File

@@ -30,8 +30,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return
}
console.log(event.type)
// Store donation data when payment intent is valid
// Subscriptions are handled on the invoice.paid event instead
if (event.type === 'payment_intent.succeeded') {
@@ -50,7 +48,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
projectName: metadata.projectName,
projectSlug: metadata.projectSlug,
fund: 'Monero Fund',
fiatAmount: paymentIntent.amount_received,
fiatAmount: paymentIntent.amount_received / 100,
membershipExpiresAt:
metadata.isMembership === 'true' ? dayjs().add(1, 'year').toDate() : null,
},
@@ -75,7 +73,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
projectName: metadata.projectName,
projectSlug: metadata.projectSlug,
fund: 'Monero Fund',
fiatAmount: invoice.total,
fiatAmount: invoice.total / 100,
membershipExpiresAt: new Date(invoiceLine.period.end * 1000),
},
})

View File

@@ -5,8 +5,8 @@ import ErrorPage from 'next/error'
import Image from 'next/image'
import xss from 'xss'
import { ProjectItem, Stats } from '../../utils/types'
import { getPostBySlug, getAllPosts } from '../../utils/md'
import { ProjectDonationStats, ProjectItem } from '../../utils/types'
import { getProjectBySlug, getAllPosts } from '../../utils/md'
import markdownToHtml from '../../utils/markdownToHtml'
import {
fetchPostJSON,
@@ -16,33 +16,24 @@ import {
import PageHeading from '../../components/PageHeading'
import SocialIcon from '../../components/social-icons'
import Progress from '../../components/Progress'
import { prisma } from '../../server/services'
import { Button } from '../../components/ui/button'
import { Dialog, DialogContent } from '../../components/ui/dialog'
import DonationFormModal from '../../components/DonationFormModal'
import MembershipFormModal from '../../components/MembershipFormModal'
import Head from 'next/head'
type SingleProjectPageProps = {
project: ProjectItem
projects: ProjectItem[]
stats: Stats
donationStats: ProjectDonationStats
}
const Project: NextPage<SingleProjectPageProps> = ({
project,
projects,
stats,
}) => {
const Project: NextPage<SingleProjectPageProps> = ({ project, projects, donationStats }) => {
const router = useRouter()
const [modalOpen, setModalOpen] = useState(false)
const [selectedProject, setSelectedProject] = useState<ProjectItem>()
function closeModal() {
setModalOpen(false)
}
function openPaymentModal() {
console.log('opening single project modal...')
setSelectedProject(project)
setModalOpen(true)
}
const [donateModalOpen, setDonateModalOpen] = useState(false)
const [memberModalOpen, setMemberModalOpen] = useState(false)
const {
slug,
@@ -81,8 +72,13 @@ const Project: NextPage<SingleProjectPageProps> = ({
if (!router.isFallback && !slug) {
return <ErrorPage statusCode={404} />
}
return (
<>
<Head>
<title>Monero Fund - {project.title}</title>
</Head>
<div className="divide-y divide-gray-200 dark:divide-gray-700">
<PageHeading project={project}>
<div className="flex flex-col items-center space-x-2 pt-8 xl:block">
@@ -95,13 +91,22 @@ const Project: NextPage<SingleProjectPageProps> = ({
/>
<div className="space-y-4">
{!project.isFunded && (
<div className="flex flex-col space-y-2">
<Button onClick={() => setDonateModalOpen(true)}>Donate</Button>
<Button onClick={() => setMemberModalOpen(true)} variant="outline">
Get Annual Membership
</Button>
</div>
)}
<h1 className="mb-4 font-bold">Raised</h1>
<Progress
percent={Math.floor(
((stats[slug].xmr.totaldonationsinfiat +
stats[slug].btc.totaldonationsinfiat +
stats[slug].usd.totaldonationsinfiat) /
((donationStats.xmr.fiatAmount +
donationStats.btc.fiatAmount +
donationStats.usd.fiatAmount) /
goal) *
100
)}
@@ -109,34 +114,28 @@ const Project: NextPage<SingleProjectPageProps> = ({
<ul className="font-semibold space-y-1">
<li className="flex items-center space-x-1">
<span className="text-green-500 text-xl">{`${formatUsd(stats[slug].xmr.totaldonationsinfiat + stats[slug].btc.totaldonationsinfiat + stats[slug].usd.totaldonationsinfiat)}`}</span>{' '}
<span className="text-green-500 text-xl">{`${formatUsd(donationStats.xmr.fiatAmount + donationStats.btc.fiatAmount + donationStats.usd.fiatAmount)}`}</span>{' '}
<span className="font-normal text-sm text-gray">
in{' '}
{stats[slug].xmr.numdonations +
stats[slug].btc.numdonations +
stats[slug].usd.numdonations}{' '}
in {donationStats.xmr.count + donationStats.btc.count + donationStats.usd.count}{' '}
donations total
</span>
</li>
<li>
{stats[slug].xmr.totaldonations} XMR{' '}
{donationStats.xmr.amount} XMR{' '}
<span className="font-normal text-sm text-gray">
in {stats[slug].xmr.numdonations} {''}
donations
in {donationStats.xmr.count} donations
</span>
</li>
<li>
{stats[slug].btc.totaldonations} BTC{' '}
{donationStats.btc.amount} BTC{' '}
<span className="font-normal text-sm text-gray">
in {stats[slug].btc.numdonations} {''}
donations
in {donationStats.btc.count} donations
</span>
</li>
<li>
{`${formatUsd(stats[slug].usd.totaldonations)}`} Fiat{' '}
{`${formatUsd(donationStats.usd.amount)}`} Fiat{' '}
<span className="font-normal text-sm text-gray">
in {stats[slug].usd.numdonations} {''}
donations
in {donationStats.usd.count} donations
</span>
</li>
</ul>
@@ -150,82 +149,17 @@ const Project: NextPage<SingleProjectPageProps> = ({
</PageHeading>
</div>
{/* <div className="flex flex-col items-center">
<div
className={
'h-[15rem] w-full relative bg-gradient-to-b from-white to-gray-200'
}
>
<Image
alt={title}
src={coverImage}
layout="fill"
objectFit="contain"
objectPosition="50% 50%"
/>
</div>
<Dialog open={donateModalOpen} onOpenChange={setDonateModalOpen}>
<DialogContent>
<DonationFormModal project={project} />
</DialogContent>
</Dialog>
<div className="flex w-full p-4 py-8 md:px-8"></div>
<article className="px-4 md:px-8 pb-8 lg:flex lg:flex-row-reverse lg:items-start">
<div className={markdownStyles['markdown']}>
</div>
<aside className="p-4 bg-light rounded-xl flex lg:flex-col lg:items-start gap-4 min-w-[20rem] justify-between items-center mb-8">
{isFunded ? `` : <button onClick={openPaymentModal}>Donate</button>}
{stats && (
<div>
<h5>Raised</h5>
<h4>{`${formatUsd(stats[slug].xmr.totaldonationsinfiat + stats[slug].btc.totaldonationsinfiat + stats[slug].usd.totaldonationsinfiat)}`}</h4>
<h6>{stats[slug].xmr.totaldonations} XMR</h6>
<h6>{stats[slug].btc.totaldonations} BTC</h6>
<h6>{`${formatUsd(stats[slug].usd.totaldonations)}`} Fiat</h6>
</div>
)}
{stats && (
<div>
<h5>Donations</h5>
<h4>
{stats[slug].xmr.numdonations +
stats[slug].btc.numdonations +
stats[slug].usd.numdonations}
</h4>
<h6>{stats[slug].xmr.numdonations} in XMR</h6>
<h6>{stats[slug].btc.numdonations} in BTC</h6>
<h6>{stats[slug].usd.numdonations} in Fiat</h6>
<Progress
text={Math.floor(
((stats[slug].xmr.totaldonationsinfiat +
stats[slug].btc.totaldonationsinfiat +
stats[slug].usd.totaldonationsinfiat) /
goal) *
100
)}
></Progress>
</div>
}
</aside>
</article>
<aside className="bg-light mb-8 flex min-w-[20rem] items-center justify-between gap-4 rounded-xl p-4 lg:flex-col lg:items-start">
{!isFunded && (
<button
onClick={openPaymentModal}
className="block rounded border border-stone-800 bg-stone-800 px-4 py-2 font-semibold text-white hover:border-transparent hover:bg-orange-500 hover:text-stone-800 dark:bg-white dark:text-black dark:hover:bg-orange-500"
>
Donate
</button>
)}
</aside>
</div> */}
{/* <PaymentModal
isOpen={modalOpen}
onRequestClose={closeModal}
project={selectedProject}
/> */}
<Dialog open={memberModalOpen} onOpenChange={setMemberModalOpen}>
<DialogContent>
<MembershipFormModal project={project} />
</DialogContent>
</Dialog>
</>
)
}
@@ -233,53 +167,59 @@ const Project: NextPage<SingleProjectPageProps> = ({
export default Project
export async function getServerSideProps({ params }: { params: any }) {
const post = getPostBySlug(params.slug)
const project = getProjectBySlug(params.slug)
const content = await markdownToHtml(project.content || '')
const projects = getAllPosts()
const donationStats = {
xmr: {
count: project.isFunded ? project.numdonationsxmr : 0,
amount: project.isFunded ? project.totaldonationsxmr : 0,
fiatAmount: project.isFunded ? project.totaldonationsinfiatxmr : 0,
},
btc: {
count: project.isFunded ? project.numdonationsbtc : 0,
amount: project.isFunded ? project.totaldonationsbtc : 0,
fiatAmount: project.isFunded ? project.totaldonationsinfiatbtc : 0,
},
usd: {
count: project.isFunded ? project.fiatnumdonations : 0,
amount: project.isFunded ? project.fiattotaldonations : 0,
fiatAmount: project.isFunded ? project.fiattotaldonationsinfiat : 0,
},
}
const content = await markdownToHtml(post.content || '')
if (!project.isFunded) {
const donations = await prisma.donation.findMany({ where: { projectSlug: params.slug } })
let stats: any = {}
for (let i = 0; i < projects.length; i++) {
let xmr
let btc
let usd
if (projects[i].isFunded) {
xmr = {
numdonations: projects[i].numdonationsxmr,
totaldonationsinfiat: projects[i].totaldonationsinfiatxmr,
totaldonations: projects[i].totaldonationsxmr,
donations.forEach((donation) => {
if (donation.cryptoCode === 'XMR') {
donationStats.xmr.count += 1
donationStats.xmr.amount += donation.cryptoAmount
donationStats.xmr.fiatAmount += donation.fiatAmount
}
btc = {
numdonations: projects[i].numdonationsbtc,
totaldonationsinfiat: projects[i].totaldonationsinfiatbtc,
totaldonations: projects[i].totaldonationsbtc,
}
usd = {
numdonations: projects[i].fiatnumdonations,
totaldonationsinfiat: projects[i].fiattotaldonationsinfiat,
totaldonations: projects[i].fiattotaldonations,
}
} else {
const crypto = await fetchGetJSONAuthedBTCPay(projects[i].slug)
xmr = await crypto.xmr
btc = await crypto.btc
usd = await fetchGetJSONAuthedStripe(projects[i].slug)
}
stats[projects[i].slug] = { xmr, btc, usd }
if (donation.cryptoCode === 'BTC') {
donationStats.btc.count += 1
donationStats.btc.amount += donation.cryptoAmount
donationStats.btc.fiatAmount += donation.fiatAmount
}
if (donation.cryptoCode === null) {
donationStats.usd.count += 1
donationStats.usd.amount += donation.fiatAmount
donationStats.usd.fiatAmount += donation.fiatAmount
console.log(donation)
}
})
}
return {
props: {
project: {
...post,
...project,
content,
},
projects,
stats,
donationStats,
},
}
}

View File

@@ -11,8 +11,9 @@ CREATE TABLE "Donation" (
"projectSlug" TEXT NOT NULL,
"projectName" TEXT NOT NULL,
"fund" TEXT NOT NULL,
"fiatAmount" INTEGER NOT NULL,
"cryptoCode" TEXT,
"fiatAmount" DOUBLE PRECISION NOT NULL,
"cryptoAmount" DOUBLE PRECISION,
"membershipExpiresAt" TIMESTAMP(3),
CONSTRAINT "Donation_pkey" PRIMARY KEY ("id")

View File

@@ -26,8 +26,9 @@ model Donation {
projectSlug String
projectName String
fund String
fiatAmount Int
cryptoCode String?
fiatAmount Float
cryptoAmount Float?
membershipExpiresAt DateTime?
@@index([stripePaymentIntentId])

View File

@@ -19,10 +19,7 @@ export const donationRouter = router({
email: z.string().email().nullable(),
projectName: z.string().min(1),
projectSlug: z.string().min(1),
amount: z
.number()
.min(MIN_AMOUNT / 100)
.max(MAX_AMOUNT / 100),
amount: z.number().min(MIN_AMOUNT).max(MAX_AMOUNT),
})
)
.mutation(async ({ input, ctx }) => {
@@ -99,10 +96,7 @@ export const donationRouter = router({
email: z.string().trim().email().nullable(),
projectName: z.string().min(1),
projectSlug: z.string().min(1),
amount: z
.number()
.min(MIN_AMOUNT / 100)
.max(MAX_AMOUNT / 100),
amount: z.number().min(MIN_AMOUNT).max(MAX_AMOUNT),
})
)
.mutation(async ({ input, ctx }) => {
@@ -284,7 +278,7 @@ export const donationRouter = router({
}
const response = await btcpayApi.post(`/stores/${env.BTCPAY_STORE_ID}/invoices`, {
amount: MEMBERSHIP_PRICE / 100,
amount: MEMBERSHIP_PRICE,
currency: CURRENCY,
metadata,
checkout: { redirectURL: `${env.APP_URL}/thankyou` },

View File

@@ -38,11 +38,7 @@ export async function fetchPostJSON(url: string, data?: {}) {
}
}
export async function fetchPostJSONAuthed(
url: string,
auth: string,
data?: {}
) {
export async function fetchPostJSONAuthed(url: string, auth: string, data?: {}) {
try {
// Default options are marked with *
const response = await fetch(url, {
@@ -86,10 +82,7 @@ export async function fetchGetJSONAuthedBTCPay(slug: string) {
let totaldonationsinfiatxmr = 0
let totaldonationsinfiatbtc = 0
for (let i = 0; i < data.length; i++) {
if (
data[i].metadata.orderId != slug &&
data[i].metadata.orderId != `${slug}_STATIC`
) {
if (data[i].metadata.orderId != slug && data[i].metadata.orderId != `${slug}_STATIC`) {
continue
}
const id = data[i].id
@@ -103,23 +96,15 @@ export async function fetchGetJSONAuthedBTCPay(slug: string) {
},
})
const dataiter = await responseiter.json()
if (
dataiter[1].cryptoCode == 'XMR' &&
dataiter[1].paymentMethodPaid > 0
) {
if (dataiter[1].cryptoCode == 'XMR' && dataiter[1].paymentMethodPaid > 0) {
numdonationsxmr += dataiter[1].payments.length
totaldonationsxmr += Number(dataiter[1].paymentMethodPaid)
totaldonationsinfiatxmr +=
Number(dataiter[1].paymentMethodPaid) * Number(dataiter[1].rate)
totaldonationsinfiatxmr += Number(dataiter[1].paymentMethodPaid) * Number(dataiter[1].rate)
}
if (
dataiter[0].cryptoCode == 'BTC' &&
dataiter[0].paymentMethodPaid > 0
) {
if (dataiter[0].cryptoCode == 'BTC' && dataiter[0].paymentMethodPaid > 0) {
numdonationsbtc += dataiter[0].payments.length
totaldonationsbtc += Number(dataiter[0].paymentMethodPaid)
totaldonationsinfiatbtc +=
Number(dataiter[0].paymentMethodPaid) * Number(dataiter[0].rate)
totaldonationsinfiatbtc += Number(dataiter[0].paymentMethodPaid) * Number(dataiter[0].rate)
}
}
return await {
@@ -158,10 +143,7 @@ export async function fetchGetJSONAuthedStripe(slug: string) {
let total = 0
let donations = 0
for (let i = 0; i < dataext.length; i++) {
if (
dataext[i].metadata.project_slug == null ||
dataext[i].metadata.project_slug != slug
) {
if (dataext[i].metadata.project_slug == null || dataext[i].metadata.project_slug != slug) {
continue
}
total += Number(dataext[i].amount) / 100

View File

@@ -5,7 +5,8 @@ import sanitize from 'sanitize-filename'
const postsDirectory = join(process.cwd(), 'docs/projects')
const FIELDS = ['title',
const FIELDS = [
'title',
'summary',
'slug',
'git',
@@ -40,8 +41,8 @@ export function getSingleFile(path: string) {
return fs.readFileSync(fullPath, 'utf8')
}
export function getPostBySlug(slug: string) {
const fields = FIELDS;
export function getProjectBySlug(slug: string) {
const fields = FIELDS
const realSlug = slug.replace(/\.md$/, '')
const fullPath = join(postsDirectory, `${sanitize(realSlug)}.md`)
const fileContents = fs.readFileSync(fullPath, 'utf8')
@@ -68,7 +69,7 @@ export function getPostBySlug(slug: string) {
export function getAllPosts() {
const slugs = getPostSlugs()
const posts = slugs.map((slug) => getPostBySlug(slug))
const posts = slugs.map((slug) => getProjectBySlug(slug))
return posts
}

View File

@@ -33,4 +33,20 @@ export type PayReq = {
name?: string
}
export type Stats = any
export type ProjectDonationStats = {
xmr: {
count: number
amount: number
fiatAmount: number
}
btc: {
count: number
amount: number
fiatAmount: number
}
usd: {
count: number
amount: number
fiatAmount: number
}
}