mirror of
https://github.com/MAGICGrants/campaign-site.git
synced 2026-01-09 12:27:59 -05:00
Campaign page goal improvements
This commit is contained in:
@@ -1,27 +1,45 @@
|
||||
import { networkFor, SocialIcon } from 'react-social-icons'
|
||||
import { ReactNode } from 'react'
|
||||
import { ReactNode, SVGProps } from 'react'
|
||||
import { FundSlug } from '@prisma/client'
|
||||
import Image from 'next/image'
|
||||
|
||||
import { ProjectItem } from '../utils/types'
|
||||
import CustomLink from './CustomLink'
|
||||
import WebIcon from './WebIcon'
|
||||
import MagicLogo from './MagicLogo'
|
||||
import MoneroLogo from './MoneroLogo'
|
||||
import FiroLogo from './FiroLogo'
|
||||
import PrivacyGuidesLogo from './PrivacyGuidesLogo'
|
||||
|
||||
interface Props {
|
||||
project: ProjectItem
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const placeholderImages: Record<FundSlug, (props: SVGProps<SVGSVGElement>) => JSX.Element> = {
|
||||
monero: MoneroLogo,
|
||||
firo: FiroLogo,
|
||||
privacyguides: PrivacyGuidesLogo,
|
||||
general: MagicLogo,
|
||||
}
|
||||
|
||||
export default function PageHeading({ project, children }: Props) {
|
||||
const PlaceholderImage = placeholderImages[project.fund]
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200">
|
||||
<div className="items-start space-y-2 pb-8 pt-6 md:space-y-5 xl:grid xl:grid-cols-3 xl:gap-x-8">
|
||||
<Image
|
||||
src={project.coverImage}
|
||||
alt="avatar"
|
||||
width={300}
|
||||
height={300}
|
||||
className="h-60 w-60 mx-auto my-auto object-contain row-span-3 hidden xl:block"
|
||||
/>
|
||||
{project.coverImage ? (
|
||||
<Image
|
||||
src={project.coverImage}
|
||||
alt="avatar"
|
||||
width={300}
|
||||
height={300}
|
||||
className="h-60 w-60 mx-auto my-auto object-contain row-span-3 hidden xl:block"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage className="w-60 h-60 mx-auto my-auto object-contain row-span-3 hidden xl:block" />
|
||||
)}
|
||||
|
||||
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14 xl:col-span-2">
|
||||
{!!project.website && (
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
type ProgressProps = { current: number; goal: number }
|
||||
import { formatUsd } from '../utils/money-formating'
|
||||
|
||||
const Progress = ({ current, goal }: ProgressProps) => {
|
||||
type ProgressProps = { current: number; goal: number; percentOnly?: boolean }
|
||||
|
||||
const numberFormat = Intl.NumberFormat('en', { notation: 'compact', compactDisplay: 'short' })
|
||||
|
||||
const Progress = ({ current, goal, percentOnly }: ProgressProps) => {
|
||||
const percent = Math.floor((current / goal) * 100)
|
||||
|
||||
return (
|
||||
@@ -12,7 +16,14 @@ const Progress = ({ current, goal }: ProgressProps) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className="text-sm font-semibold">{percent < 100 ? percent : 100}%</span>
|
||||
<span className="text-sm">
|
||||
Raised <strong>{percent < 100 ? percent : 100}%</strong>{' '}
|
||||
{!percentOnly && (
|
||||
<>
|
||||
of <strong className="text-green-500">${numberFormat.format(goal)}</strong> Goal
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, SVGProps } from 'react'
|
||||
import { FundSlug } from '@prisma/client'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { ProjectItem } from '../utils/types'
|
||||
import { cn } from '../utils/cn'
|
||||
import Progress from './Progress'
|
||||
import MoneroLogo from './MoneroLogo'
|
||||
import FiroLogo from './FiroLogo'
|
||||
import PrivacyGuidesLogo from './PrivacyGuidesLogo'
|
||||
import MagicLogo from './MagicLogo'
|
||||
|
||||
const numberFormat = Intl.NumberFormat('en', { notation: 'compact', compactDisplay: 'short' })
|
||||
|
||||
@@ -13,21 +18,15 @@ export type ProjectCardProps = {
|
||||
customImageStyles?: React.CSSProperties
|
||||
}
|
||||
|
||||
const placeholderImages: Record<FundSlug, (props: SVGProps<SVGSVGElement>) => JSX.Element> = {
|
||||
monero: MoneroLogo,
|
||||
firo: FiroLogo,
|
||||
privacyguides: PrivacyGuidesLogo,
|
||||
general: MagicLogo,
|
||||
}
|
||||
|
||||
const ProjectCard: React.FC<ProjectCardProps> = ({ project, customImageStyles }) => {
|
||||
const [isHorizontal, setIsHorizontal] = useState<boolean | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const img = document.createElement('img')
|
||||
img.src = project.coverImage
|
||||
|
||||
// check if image is horizontal - added additional 10% to height to ensure only true
|
||||
// horizontals get flagged.
|
||||
img.onload = () => {
|
||||
const { naturalWidth, naturalHeight } = img
|
||||
const isHorizontal = naturalWidth >= naturalHeight * 1.1
|
||||
setIsHorizontal(isHorizontal)
|
||||
}
|
||||
}, [project.coverImage])
|
||||
const PlaceholderImage = placeholderImages[project.fund]
|
||||
|
||||
return (
|
||||
<Link href={`/${project.fund}/projects/${project.slug}`} passHref target="_blank">
|
||||
@@ -40,16 +39,20 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project, customImageStyles })
|
||||
project.fund === 'general' && 'border-primary'
|
||||
)}
|
||||
>
|
||||
<div className="flex h-36 w-full sm:h-52">
|
||||
<Image
|
||||
alt={project.title}
|
||||
src={project.coverImage}
|
||||
width={1200}
|
||||
height={1200}
|
||||
style={{ objectFit: 'contain', ...customImageStyles }}
|
||||
priority={true}
|
||||
className="cursor-pointer rounded-t-xl bg-white"
|
||||
/>
|
||||
<div className="flex h-48 w-full sm:h-52">
|
||||
{project.coverImage ? (
|
||||
<Image
|
||||
alt={project.title}
|
||||
src={project.coverImage}
|
||||
width={700}
|
||||
height={700}
|
||||
style={{ objectFit: 'contain', ...customImageStyles }}
|
||||
priority={true}
|
||||
className="cursor-pointer rounded-t-xl bg-white"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage className="w-1/2 h-full max-h-full m-auto cursor-pointer rounded-t-xl bg-white" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<figcaption className="p-5 flex flex-col grow space-y-4 justify-between">
|
||||
@@ -73,6 +76,7 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project, customImageStyles })
|
||||
project.totalDonationsFiat
|
||||
}
|
||||
goal={project.goal}
|
||||
percentOnly
|
||||
/>
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { GetStaticProps, GetStaticPropsContext } from 'next'
|
||||
import { SVGProps, useEffect, useRef, useState } from 'react'
|
||||
import { GetStaticPropsContext } from 'next'
|
||||
import Link from 'next/link'
|
||||
import Head from 'next/head'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { faMonero } from '@fortawesome/free-brands-svg-icons'
|
||||
@@ -12,7 +14,6 @@ import { z } from 'zod'
|
||||
import Image from 'next/image'
|
||||
|
||||
import { MAX_AMOUNT } from '../../../config'
|
||||
import { useFundSlug } from '../../../utils/use-fund-slug'
|
||||
import { trpc } from '../../../utils/trpc'
|
||||
import Spinner from '../../../components/Spinner'
|
||||
import { useToast } from '../../../components/ui/use-toast'
|
||||
@@ -32,15 +33,25 @@ import CustomLink from '../../../components/CustomLink'
|
||||
import { getProjectBySlug, getProjects } from '../../../utils/md'
|
||||
import { funds, fundSlugs } from '../../../utils/funds'
|
||||
import { ProjectItem } from '../../../utils/types'
|
||||
import Link from 'next/link'
|
||||
import Head from 'next/head'
|
||||
import MoneroLogo from '../../../components/MoneroLogo'
|
||||
import FiroLogo from '../../../components/FiroLogo'
|
||||
import PrivacyGuidesLogo from '../../../components/PrivacyGuidesLogo'
|
||||
import MagicLogo from '../../../components/MagicLogo'
|
||||
|
||||
type QueryParams = { fund: FundSlug; slug: string }
|
||||
type Props = { project: ProjectItem } & QueryParams
|
||||
|
||||
const placeholderImages: Record<FundSlug, (props: SVGProps<SVGSVGElement>) => JSX.Element> = {
|
||||
monero: MoneroLogo,
|
||||
firo: FiroLogo,
|
||||
privacyguides: PrivacyGuidesLogo,
|
||||
general: MagicLogo,
|
||||
}
|
||||
|
||||
function DonationPage({ fund: fundSlug, slug, project }: Props) {
|
||||
const session = useSession()
|
||||
const isAuthed = session.status === 'authenticated'
|
||||
const PlaceholderImage = placeholderImages[project.fund]
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
@@ -153,14 +164,21 @@ function DonationPage({ fund: fundSlug, slug, project }: Props) {
|
||||
<div className="max-w-[540px] mx-auto p-6 space-y-6 rounded-lg bg-white">
|
||||
<div className="py-4 flex flex-col space-y-6">
|
||||
<div className="flex flex-col items-center sm:space-x-4 sm:flex-row">
|
||||
<Image
|
||||
alt={project.title}
|
||||
src={project.coverImage}
|
||||
width={200}
|
||||
height={96}
|
||||
objectFit="cover"
|
||||
className="w-36 rounded-lg"
|
||||
/>
|
||||
{project.coverImage ? (
|
||||
<Image
|
||||
alt={project.title}
|
||||
src={project.coverImage}
|
||||
width={200}
|
||||
height={96}
|
||||
objectFit="cover"
|
||||
className="w-36 rounded-lg"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-52">
|
||||
<PlaceholderImage className="w-20 h-20 m-auto" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col justify-center">
|
||||
<h2 className="text-center sm:text-left font-semibold">Donate to {project.title}</h2>
|
||||
<h3 className="text-gray-500">Pledge your support</h3>
|
||||
|
||||
@@ -189,7 +189,7 @@ function MembershipPage({ fund: fundSlug, project }: Props) {
|
||||
<div className="flex flex-col items-center sm:space-x-4 sm:flex-row">
|
||||
<Image
|
||||
alt={project.title}
|
||||
src={project.coverImage}
|
||||
src={project.coverImage!}
|
||||
width={200}
|
||||
height={96}
|
||||
objectFit="cover"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { SVGProps } from 'react'
|
||||
import { FundSlug } from '@prisma/client'
|
||||
import { useRouter } from 'next/router'
|
||||
import { GetServerSidePropsContext, NextPage } from 'next/types'
|
||||
import Head from 'next/head'
|
||||
@@ -14,11 +16,16 @@ import Progress from '../../../components/Progress'
|
||||
import { prisma } from '../../../server/services'
|
||||
import { Button } from '../../../components/ui/button'
|
||||
import { trpc } from '../../../utils/trpc'
|
||||
import { getFundSlugFromUrlPath } from '../../../utils/funds'
|
||||
import { funds, getFundSlugFromUrlPath } from '../../../utils/funds'
|
||||
import { useFundSlug } from '../../../utils/use-fund-slug'
|
||||
import { Table, TableBody, TableCell, TableRow } from '../../../components/ui/table'
|
||||
import { cn } from '../../../utils/cn'
|
||||
import { DonationCryptoPayments } from '../../../server/types'
|
||||
import { formatBtc, formatUsd } from '../../../utils/money-formating'
|
||||
import MagicLogo from '../../../components/MagicLogo'
|
||||
import MoneroLogo from '../../../components/MoneroLogo'
|
||||
import FiroLogo from '../../../components/FiroLogo'
|
||||
import PrivacyGuidesLogo from '../../../components/PrivacyGuidesLogo'
|
||||
|
||||
type SingleProjectPageProps = {
|
||||
project: ProjectItem
|
||||
@@ -26,29 +33,20 @@ type SingleProjectPageProps = {
|
||||
donationStats: ProjectDonationStats
|
||||
}
|
||||
|
||||
const placeholderImages: Record<FundSlug, (props: SVGProps<SVGSVGElement>) => JSX.Element> = {
|
||||
monero: MoneroLogo,
|
||||
firo: FiroLogo,
|
||||
privacyguides: PrivacyGuidesLogo,
|
||||
general: MagicLogo,
|
||||
}
|
||||
|
||||
const Project: NextPage<SingleProjectPageProps> = ({ project, donationStats }) => {
|
||||
const router = useRouter()
|
||||
const fundSlug = useFundSlug()
|
||||
|
||||
const { slug, title, summary, coverImage, content, nym, website, goal, isFunded } = project
|
||||
|
||||
function formatBtc(bitcoin: number) {
|
||||
if (bitcoin > 0.1) {
|
||||
return `${bitcoin.toFixed(3) || 0.0} BTC`
|
||||
} else {
|
||||
return `${Math.floor(bitcoin * 100000000).toLocaleString()} sats`
|
||||
}
|
||||
}
|
||||
|
||||
function formatUsd(dollars: number): string {
|
||||
if (dollars == 0) {
|
||||
return '$0'
|
||||
} else if (dollars / 1000 > 1) {
|
||||
return `$${Math.round(dollars / 1000)}k+`
|
||||
} else {
|
||||
return `$${dollars.toFixed(0)}`
|
||||
}
|
||||
}
|
||||
const PlaceholderImage = placeholderImages[project.fund]
|
||||
|
||||
if (!router.isFallback && !slug) {
|
||||
return <ErrorPage statusCode={404} />
|
||||
@@ -62,21 +60,27 @@ const Project: NextPage<SingleProjectPageProps> = ({ project, donationStats }) =
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Monero Fund | {project.title}</title>
|
||||
<title>
|
||||
{project.title} - {funds[project.fund].title}
|
||||
</title>
|
||||
</Head>
|
||||
|
||||
<div className="divide-y divide-gray-200">
|
||||
<PageHeading project={project}>
|
||||
<div className="w-full flex flex-col items-center gap-4 xl:flex">
|
||||
<Image
|
||||
src={coverImage}
|
||||
alt="avatar"
|
||||
width={700}
|
||||
height={700}
|
||||
className="w-full max-w-[700px] mx-auto object-contain xl:hidden"
|
||||
/>
|
||||
{coverImage ? (
|
||||
<Image
|
||||
src={coverImage}
|
||||
alt="avatar"
|
||||
width={700}
|
||||
height={700}
|
||||
className="w-full max-w-[700px] mx-auto object-contain xl:hidden"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage className="w-full max-w-[700px] mx-auto object-contain xl:hidden" />
|
||||
)}
|
||||
|
||||
<div className="w-full max-w-96 space-y-8 p-6 bg-white rounded-lg">
|
||||
<div className="w-full max-w-96 space-y-6 p-6 bg-white rounded-lg">
|
||||
{!project.isFunded && (
|
||||
<div className="w-full">
|
||||
<Link href={`/${fundSlug}/donate/${project.slug}`}>
|
||||
@@ -85,57 +89,52 @@ const Project: NextPage<SingleProjectPageProps> = ({ project, donationStats }) =
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="w-full">
|
||||
<h1 className="mb-4 font-bold">Raised</h1>
|
||||
<Progress
|
||||
current={
|
||||
donationStats.xmr.fiatAmount +
|
||||
donationStats.btc.fiatAmount +
|
||||
donationStats.usd.fiatAmount
|
||||
}
|
||||
goal={goal}
|
||||
/>
|
||||
|
||||
<Progress
|
||||
current={
|
||||
donationStats.xmr.fiatAmount +
|
||||
donationStats.btc.fiatAmount +
|
||||
donationStats.usd.fiatAmount
|
||||
}
|
||||
goal={goal}
|
||||
/>
|
||||
|
||||
<ul className="font-semibold space-y-1">
|
||||
<li className="flex items-center space-x-1">
|
||||
<span className="text-green-500 text-xl">{`${formatUsd(donationStats.xmr.fiatAmount + donationStats.btc.fiatAmount + donationStats.ltc.fiatAmount + donationStats.manual.fiatAmount + donationStats.usd.fiatAmount)}`}</span>{' '}
|
||||
<span className="font-normal text-sm text-gray">
|
||||
in{' '}
|
||||
{donationStats.xmr.count +
|
||||
donationStats.btc.count +
|
||||
donationStats.manual.count +
|
||||
donationStats.usd.count}{' '}
|
||||
donations total
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
{donationStats.xmr.amount} XMR{' '}
|
||||
<span className="font-normal text-sm text-gray">
|
||||
in {donationStats.xmr.count} donations
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
{formatBtc(donationStats.btc.amount)}{' '}
|
||||
<span className="font-normal text-sm text-gray">
|
||||
in {donationStats.btc.count} donations
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
{donationStats.ltc.amount} LTC{' '}
|
||||
<span className="font-normal text-sm text-gray">
|
||||
in {donationStats.ltc.count} donations
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
{`${formatUsd(donationStats.usd.amount + donationStats.manual.fiatAmount)}`}{' '}
|
||||
Fiat{' '}
|
||||
<span className="font-normal text-sm text-gray">
|
||||
in {donationStats.usd.count + donationStats.manual.count} donations
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<ul className="font-semibold">
|
||||
<li className="flex items-center space-x-1">
|
||||
<span className="text-green-500 text-xl">{`${formatUsd(donationStats.xmr.fiatAmount + donationStats.btc.fiatAmount + donationStats.ltc.fiatAmount + donationStats.manual.fiatAmount + donationStats.usd.fiatAmount)}`}</span>{' '}
|
||||
<span className="font-normal text-sm text-gray">
|
||||
in{' '}
|
||||
{donationStats.xmr.count +
|
||||
donationStats.btc.count +
|
||||
donationStats.manual.count +
|
||||
donationStats.usd.count}{' '}
|
||||
donations total
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
{donationStats.xmr.amount.toFixed(2)} XMR{' '}
|
||||
<span className="font-normal text-sm text-gray">
|
||||
in {donationStats.xmr.count} donations
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
{formatBtc(donationStats.btc.amount)}{' '}
|
||||
<span className="font-normal text-sm text-gray">
|
||||
in {donationStats.btc.count} donations
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
{donationStats.ltc.amount.toFixed(2)} LTC{' '}
|
||||
<span className="font-normal text-sm text-gray">
|
||||
in {donationStats.ltc.count} donations
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
{`${formatUsd(donationStats.usd.amount + donationStats.manual.fiatAmount)}`}{' '}
|
||||
<span className="font-normal text-sm text-gray">
|
||||
in {donationStats.usd.count + donationStats.manual.count} donations
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-96 min-h-72 space-y-4 p-6 bg-white rounded-lg">
|
||||
|
||||
17
utils/money-formating.ts
Normal file
17
utils/money-formating.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export function formatUsd(dollars: number): string {
|
||||
if (dollars == 0) {
|
||||
return '$0'
|
||||
} else if (dollars / 1000 > 1) {
|
||||
return `$${Math.round(dollars / 1000)}k+`
|
||||
} else {
|
||||
return `$${dollars.toFixed(0)}`
|
||||
}
|
||||
}
|
||||
|
||||
export function formatBtc(bitcoin: number) {
|
||||
if (bitcoin > 0.1) {
|
||||
return `${bitcoin.toFixed(3) || 0.0} BTC`
|
||||
} else {
|
||||
return `${Math.floor(bitcoin * 100000000).toLocaleString()} sats`
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export type ProjectItem = {
|
||||
content?: string
|
||||
title: string
|
||||
summary: string
|
||||
coverImage: string
|
||||
coverImage?: string
|
||||
website: string
|
||||
socialLinks: string[]
|
||||
date: string
|
||||
|
||||
Reference in New Issue
Block a user