mirror of
https://github.com/MAGICGrants/campaign-site.git
synced 2026-01-08 03:53:59 -05:00
Donation form fixes and improvements
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
/* eslint-disable jsx-a11y/anchor-has-content */
|
||||
import Link from 'next/link'
|
||||
import { AnchorHTMLAttributes, DetailedHTMLProps } from 'react'
|
||||
import { cn } from '../utils/cn'
|
||||
|
||||
const CustomLink = ({
|
||||
href,
|
||||
className,
|
||||
...rest
|
||||
}: DetailedHTMLProps<
|
||||
AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||
@@ -14,14 +16,40 @@ const CustomLink = ({
|
||||
|
||||
if (isInternalLink) {
|
||||
// @ts-ignore
|
||||
return <Link href={href} {...rest} />
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className={cn(
|
||||
'text-primary hover:text-primary-DEFAULT_HOVER',
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (isAnchorLink) {
|
||||
return <a href={href} {...rest} />
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className={cn(
|
||||
'text-primary hover:text-primary-DEFAULT_HOVER',
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <a target="_blank" rel="noopener noreferrer" href={href} {...rest} />
|
||||
return (
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn('text-primary hover:text-primary-DEFAULT_HOVER', className)}
|
||||
href={href}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomLink
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
import { faMonero } from '@fortawesome/free-brands-svg-icons'
|
||||
import { faCreditCard } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { MAX_AMOUNT } from '../config'
|
||||
import { fetchPostJSON } from '../utils/api-helpers'
|
||||
import Spinner from './Spinner'
|
||||
import { trpc } from '../utils/trpc'
|
||||
import { useToast } from './ui/use-toast'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { Button } from './ui/button'
|
||||
import { RadioGroup, RadioGroupItem } from './ui/radio-group'
|
||||
import { Label } from './ui/label'
|
||||
|
||||
type DonationStepsProps = {
|
||||
projectNamePretty: string
|
||||
projectSlug: string
|
||||
}
|
||||
const DonationSteps: React.FC<DonationStepsProps> = ({
|
||||
projectNamePretty,
|
||||
projectSlug,
|
||||
}) => {
|
||||
const { toast } = useToast()
|
||||
const session = useSession()
|
||||
console.log(session.status)
|
||||
|
||||
const [name, setName] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
|
||||
const [deductible, setDeductible] = useState('no')
|
||||
const [amount, setAmount] = useState('')
|
||||
|
||||
const [readyToPay, setReadyToPay] = useState(false)
|
||||
|
||||
const [btcPayLoading, setBtcpayLoading] = useState(false)
|
||||
const [fiatLoading, setFiatLoading] = useState(false)
|
||||
|
||||
const donateWithFiatMutation = trpc.donation.donateWithFiat.useMutation()
|
||||
const donateWithCryptoMutation = trpc.donation.donateWithCrypto.useMutation()
|
||||
|
||||
const formRef = useRef<HTMLFormElement | null>(null)
|
||||
|
||||
const radioHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDeductible(event.target.value)
|
||||
}
|
||||
|
||||
function handleFiatAmountClick(e: React.MouseEvent, value: string) {
|
||||
e.preventDefault()
|
||||
setAmount(value)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (amount && typeof parseInt(amount) === 'number') {
|
||||
if (deductible === 'no' || (name && email)) {
|
||||
setReadyToPay(true)
|
||||
} else {
|
||||
setReadyToPay(false)
|
||||
}
|
||||
} else {
|
||||
setReadyToPay(false)
|
||||
}
|
||||
}, [deductible, amount, email, name])
|
||||
|
||||
async function handleBtcPay() {
|
||||
const validity = formRef.current?.checkValidity()
|
||||
if (!validity) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await donateWithCryptoMutation.mutateAsync({
|
||||
email: email || null,
|
||||
name: name || null,
|
||||
amount: Number(amount),
|
||||
projectSlug,
|
||||
projectName: projectNamePretty,
|
||||
})
|
||||
|
||||
window.location.assign(result.url)
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFiat() {
|
||||
const validity = formRef.current?.checkValidity()
|
||||
|
||||
if (!validity) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await donateWithFiatMutation.mutateAsync({
|
||||
email: email || null,
|
||||
name: name || null,
|
||||
amount: parseInt(amount),
|
||||
projectSlug,
|
||||
projectName: projectNamePretty,
|
||||
})
|
||||
|
||||
if (!result.url) throw Error()
|
||||
|
||||
window.location.assign(result.url)
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
ref={formRef}
|
||||
className="mt-4 flex flex-col gap-4"
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
>
|
||||
<section className="flex flex-col gap-1">
|
||||
<h3>Do you want this donation to be tax deductible (USA only)?</h3>
|
||||
<div className="flex space-x-4 ">
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
id="no"
|
||||
name="deductible"
|
||||
value="no"
|
||||
onChange={radioHandler}
|
||||
defaultChecked={true}
|
||||
/>
|
||||
No
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
id="yes"
|
||||
value="yes"
|
||||
name="deductible"
|
||||
onChange={radioHandler}
|
||||
/>
|
||||
Yes
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{session.status !== 'authenticated' && (
|
||||
<>
|
||||
<h3>
|
||||
Name{' '}
|
||||
<span className="text-subtle">
|
||||
{deductible === 'yes' ? '(required)' : '(optional)'}
|
||||
</span>
|
||||
</h3>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={'MAGIC Monero Fund'}
|
||||
required={deductible === 'yes'}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="mb-4"
|
||||
></input>
|
||||
|
||||
<h3>
|
||||
Email{' '}
|
||||
<span className="text-subtle">
|
||||
{deductible === 'yes' ? '(required)' : '(optional)'}
|
||||
</span>
|
||||
</h3>
|
||||
<input
|
||||
type="email"
|
||||
placeholder={`MoneroFund@MagicGrants.org`}
|
||||
required={deductible === 'yes'}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
></input>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div className="flex justify-between items-center">
|
||||
<h3>How much would you like to donate?</h3>
|
||||
</div>
|
||||
<div className="sm:flex-row flex flex-col gap-2 py-2" role="group">
|
||||
{[50, 100, 250, 500].map((value, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className="group"
|
||||
onClick={(e) => handleFiatAmountClick(e, value.toString())}
|
||||
>
|
||||
${value}
|
||||
</button>
|
||||
))}
|
||||
<div className="relative flex w-full">
|
||||
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
|
||||
{/* <FontAwesomeIcon icon={faDollarSign} className="w-5 h-5 text-black" /> */}
|
||||
<span className="w-5 h-5 font-mono text-xl mb-2">{'$'}</span>
|
||||
</div>
|
||||
<input
|
||||
required
|
||||
type="number"
|
||||
id="amount"
|
||||
value={amount}
|
||||
onChange={(e) => {
|
||||
setAmount(e.target.value)
|
||||
}}
|
||||
className="!pl-10 w-full"
|
||||
placeholder="Or enter custom amount"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button onClick={handleBtcPay} disabled={!readyToPay || btcPayLoading}>
|
||||
{btcPayLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faMonero} className="h-5 w-5" />
|
||||
)}
|
||||
Donate with Monero
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleFiat}
|
||||
disabled={!readyToPay || donateWithFiatMutation.isPending}
|
||||
className="bg-indigo-500 hover:bg-indigo-700"
|
||||
>
|
||||
{donateWithFiatMutation.isPending ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCreditCard} className="h-5 w-5" />
|
||||
)}
|
||||
Donate with fiat
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default DonationSteps
|
||||
309
components/DonationFormModal.tsx
Normal file
309
components/DonationFormModal.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { faMonero } from '@fortawesome/free-brands-svg-icons'
|
||||
import { faCreditCard } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { MAX_AMOUNT } from '../config'
|
||||
import Spinner from './Spinner'
|
||||
import { trpc } from '../utils/trpc'
|
||||
import { useToast } from './ui/use-toast'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { Button } from './ui/button'
|
||||
import { RadioGroup, RadioGroupItem } from './ui/radio-group'
|
||||
import { Label } from './ui/label'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from './ui/form'
|
||||
import { Input } from './ui/input'
|
||||
import { DollarSign } from 'lucide-react'
|
||||
import { ProjectItem } from '../utils/types'
|
||||
import Image from 'next/image'
|
||||
import CustomLink from './CustomLink'
|
||||
|
||||
type Props = {
|
||||
project: ProjectItem | undefined
|
||||
}
|
||||
|
||||
const DonationFormModal: React.FC<Props> = ({ project }) => {
|
||||
const session = useSession()
|
||||
const isAuthed = session.status === 'authenticated'
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
email: z.string().email().optional(),
|
||||
amount: z.coerce.number().min(1).max(MAX_AMOUNT),
|
||||
taxDeductible: z.enum(['yes', 'no']),
|
||||
})
|
||||
.refine(
|
||||
(data) =>
|
||||
!isAuthed && data.taxDeductible === 'yes' ? !!data.name : true,
|
||||
{
|
||||
message: 'Name is required when the donation is tax deductible.',
|
||||
path: ['name'],
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) =>
|
||||
!isAuthed && data.taxDeductible === 'yes' ? !!data.email : true,
|
||||
{
|
||||
message: 'Email is required when the donation is tax deductible.',
|
||||
path: ['email'],
|
||||
}
|
||||
)
|
||||
|
||||
type FormInputs = z.infer<typeof schema>
|
||||
|
||||
const { toast } = useToast()
|
||||
|
||||
const form = useForm<FormInputs>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
amount: '' as unknown as number, // a trick to get trigger to work when amount is empty
|
||||
taxDeductible: 'no',
|
||||
},
|
||||
mode: 'all',
|
||||
})
|
||||
|
||||
console.log(form.getValues())
|
||||
|
||||
const taxDeductible = form.watch('taxDeductible')
|
||||
|
||||
const donateWithFiatMutation = trpc.donation.donateWithFiat.useMutation()
|
||||
const donateWithCryptoMutation = trpc.donation.donateWithCrypto.useMutation()
|
||||
|
||||
async function handleBtcPay(data: FormInputs) {
|
||||
if (!project) return
|
||||
|
||||
try {
|
||||
const result = await donateWithCryptoMutation.mutateAsync({
|
||||
email: data.email || null,
|
||||
name: data.name || null,
|
||||
amount: data.amount,
|
||||
projectSlug: project.slug,
|
||||
projectName: project.title,
|
||||
})
|
||||
|
||||
window.location.assign(result.url)
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFiat(data: FormInputs) {
|
||||
if (!project) return
|
||||
|
||||
try {
|
||||
const result = await donateWithFiatMutation.mutateAsync({
|
||||
email: data.email || null,
|
||||
name: data.name || null,
|
||||
amount: data.amount,
|
||||
projectSlug: project.slug,
|
||||
projectName: project.title,
|
||||
})
|
||||
|
||||
if (!result.url) throw Error()
|
||||
|
||||
window.location.assign(result.url)
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
form.trigger('email', { shouldFocus: true })
|
||||
form.trigger('name', { shouldFocus: true })
|
||||
}, [taxDeductible])
|
||||
|
||||
if (!project) return <></>
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col space-y-8 py-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Image
|
||||
alt={project.title}
|
||||
src={project.coverImage}
|
||||
width={96}
|
||||
height={96}
|
||||
objectFit="cover"
|
||||
className="rounded-xl"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<h2 className="font-sans font-bold">{project.title}</h2>
|
||||
<h3 className="text-textgray font-sans">Pledge your support</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form {...form}>
|
||||
<form className="flex flex-col gap-4">
|
||||
{!isAuthed && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Name {taxDeductible === 'no' && '(optional)'}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="John Doe" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Email {taxDeductible === 'no' && '(optional)'}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="johndoe@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="amount"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Amount</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex flex-row space-x-2 items-center">
|
||||
<Input
|
||||
className="w-40"
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
leftIcon={DollarSign}
|
||||
{...field}
|
||||
/>
|
||||
|
||||
{[50, 100, 250, 500].map((value, index) => (
|
||||
<Button
|
||||
key={`amount-button-${index}`}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
form.setValue('amount', value, {
|
||||
shouldValidate: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
${value}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="taxDeductible"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<FormLabel>
|
||||
Do you want this donation to be tax deductible? (US only)
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
className="flex flex-row space-x-4"
|
||||
>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="no" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">No</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="yes" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">Yes</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="mt-4 flex flex-wrap space-x-2">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={form.handleSubmit(handleBtcPay)}
|
||||
disabled={!form.formState.isValid || form.formState.isSubmitting}
|
||||
className="grow basis-0"
|
||||
>
|
||||
{donateWithCryptoMutation.isPending ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faMonero} className="h-5 w-5" />
|
||||
)}
|
||||
Donate with Monero
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={form.handleSubmit(handleFiat)}
|
||||
disabled={!form.formState.isValid || form.formState.isSubmitting}
|
||||
className="grow basis-0 bg-indigo-500 hover:bg-indigo-700"
|
||||
>
|
||||
{donateWithFiatMutation.isPending ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCreditCard} className="h-5 w-5" />
|
||||
)}
|
||||
Donate with fiat
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{!isAuthed && <div className="w-full h-px bg-border" />}
|
||||
|
||||
{!isAuthed && (
|
||||
<div className="flex flex-col items-center ">
|
||||
<p>Want to support more projects from now on?</p>
|
||||
|
||||
<Button type="button" size="lg" variant="link">
|
||||
Create an account
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DonationFormModal
|
||||
@@ -104,7 +104,12 @@ const Header = () => {
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Link href="/account/my-donations">My Donations</Link>
|
||||
<Link
|
||||
href="/account/my-donations"
|
||||
className="text-foreground hover:text-foreground"
|
||||
>
|
||||
My Donations
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => signOut({ callbackUrl: '/' })}>
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import ReactModal from 'react-modal'
|
||||
import Image from 'next/image'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faClose } from '@fortawesome/free-solid-svg-icons'
|
||||
import DonationForm from './DonationForm'
|
||||
import { ProjectItem } from '../utils/types'
|
||||
|
||||
type ModalProps = {
|
||||
isOpen: boolean
|
||||
onRequestClose: () => void
|
||||
project: ProjectItem | undefined
|
||||
}
|
||||
|
||||
const PaymentModal: React.FC<ModalProps> = ({
|
||||
isOpen,
|
||||
onRequestClose,
|
||||
project,
|
||||
}) => {
|
||||
if (!project) {
|
||||
// We never see this yeah?
|
||||
return <div />
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen={isOpen}
|
||||
onRequestClose={onRequestClose}
|
||||
className="max-h-full max-w-4xl w-full overflow-y-auto bg-white p-8 shadow-xl dark:bg-stone-800 sm:m-8 sm:rounded-xl"
|
||||
overlayClassName="inset-0 fixed bg-[rgba(0,_0,_0,_0.75)] flex items-center justify-center"
|
||||
appElement={
|
||||
typeof window === 'undefined'
|
||||
? undefined
|
||||
: document?.getElementById('root') || undefined
|
||||
}
|
||||
>
|
||||
<div className="relative -mb-12 flex justify-end">
|
||||
<FontAwesomeIcon
|
||||
icon={faClose}
|
||||
className="hover:text-primary h-[2rem] w-[2rem] cursor-pointer"
|
||||
onClick={onRequestClose}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Image
|
||||
alt={project.title}
|
||||
src={project.coverImage}
|
||||
width={96}
|
||||
height={96}
|
||||
objectFit="cover"
|
||||
className="rounded-xl"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<h2 className="font-sans font-bold">{project.title}</h2>
|
||||
<h3 className="text-textgray font-sans">Pledge your support</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DonationForm
|
||||
projectNamePretty={project.title}
|
||||
projectSlug={project.slug}
|
||||
/>
|
||||
</ReactModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaymentModal
|
||||
@@ -27,6 +27,7 @@ import { trpc } from '../utils/trpc'
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string().min(8),
|
||||
@@ -86,6 +87,20 @@ function RegisterFormModal({ close }: Props) {
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="John Doe" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
|
||||
@@ -9,7 +9,7 @@ const Spinner = () => {
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
|
||||
@@ -44,7 +44,8 @@ const useFormField = () => {
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState, formState } = useFormContext()
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
const fieldState = getFieldState(fieldContext.name)
|
||||
// console.log(fieldState, fieldContext.name)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error('useFormField should be used within <FormField>')
|
||||
@@ -144,7 +145,9 @@ const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const { error, formMessageId, ...rest } = useFormField()
|
||||
// console.log(error, rest.name)
|
||||
|
||||
const body = error ? String(error?.message) : children
|
||||
|
||||
if (!body) {
|
||||
|
||||
@@ -1,22 +1,45 @@
|
||||
import * as React from 'react'
|
||||
import { LucideIcon } from 'lucide-react'
|
||||
|
||||
import { cn } from '../../utils/cn'
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
leftIcon?: LucideIcon
|
||||
rightIcon?: LucideIcon
|
||||
}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
({ className, type, leftIcon, rightIcon, ...props }, ref) => {
|
||||
const LeftIcon = leftIcon
|
||||
const RightIcon = rightIcon
|
||||
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
<div className="relative">
|
||||
{LeftIcon && (
|
||||
<div className="absolute left-1.5 top-1/2 transform -translate-y-1/2">
|
||||
<LeftIcon size={18} className="text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||
leftIcon ? 'pl-8' : '',
|
||||
rightIcon ? 'pr-8' : '',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
{RightIcon && (
|
||||
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
<RightIcon className="text-muted-foreground" size={18} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
7
keycloak.md
Normal file
7
keycloak.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Keycloak
|
||||
|
||||
List of necessary custom attributes for the Keycloak user:
|
||||
|
||||
- `name`
|
||||
- `passwordResetTokenVersion`
|
||||
- `stripeCustomerId`
|
||||
@@ -1,16 +1,15 @@
|
||||
import type { NextPage } from 'next'
|
||||
import Head from 'next/head'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import ProjectList from '../components/ProjectList'
|
||||
import PaymentModal from '../components/PaymentModal'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { getAllPosts } from '../utils/md'
|
||||
import { ProjectItem } from '../utils/types'
|
||||
import { useRouter } from 'next/router'
|
||||
import Typing from '../components/Typing'
|
||||
import CustomLink from '../components/CustomLink'
|
||||
import { Button } from '../components/ui/button'
|
||||
import { Dialog, DialogContent } from '../components/ui/dialog'
|
||||
import DonationFormModal from '../components/DonationFormModal'
|
||||
|
||||
// These shouldn't be swept up in the regular list so we hardcode them
|
||||
const generalFund: ProjectItem = {
|
||||
@@ -29,21 +28,6 @@ const generalFund: ProjectItem = {
|
||||
const Home: NextPage<{ projects: any }> = ({ projects }) => {
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
|
||||
const [selectedProject, setSelectedProject] = useState<ProjectItem>()
|
||||
|
||||
function closeModal() {
|
||||
setModalOpen(false)
|
||||
}
|
||||
|
||||
function openPaymentModal(project: ProjectItem) {
|
||||
setSelectedProject(project)
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
function openGeneralFundModal() {
|
||||
openPaymentModal(generalFund)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -65,7 +49,7 @@ const Home: NextPage<{ projects: any }> = ({ projects }) => {
|
||||
<div className="flex flex-wrap py-4">
|
||||
<div className="w-full md:w-1/2">
|
||||
<Button
|
||||
onClick={openGeneralFundModal}
|
||||
onClick={() => setModalOpen(true)}
|
||||
size="lg"
|
||||
className="px-14 text-black font-semibold text-xl"
|
||||
>
|
||||
@@ -97,22 +81,18 @@ const Home: NextPage<{ projects: any }> = ({ projects }) => {
|
||||
</p>
|
||||
<ProjectList projects={projects} />
|
||||
<div className="flex justify-end pt-4 text-base font-medium leading-6">
|
||||
<Link
|
||||
href="/projects"
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
aria-label="View All Projects"
|
||||
>
|
||||
<CustomLink href="/projects" aria-label="View All Projects">
|
||||
View Projects →
|
||||
</Link>
|
||||
</CustomLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PaymentModal
|
||||
isOpen={modalOpen}
|
||||
onRequestClose={closeModal}
|
||||
project={selectedProject}
|
||||
/>
|
||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||
<DialogContent>
|
||||
<DonationFormModal project={generalFund} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
fetchGetJSONAuthedBTCPay,
|
||||
fetchGetJSONAuthedStripe,
|
||||
} from '../../utils/api-helpers'
|
||||
import PaymentModal from '../../components/PaymentModal'
|
||||
import PageHeading from '../../components/PageHeading'
|
||||
import SocialIcon from '../../components/social-icons'
|
||||
import Progress from '../../components/Progress'
|
||||
@@ -222,11 +221,11 @@ const Project: NextPage<SingleProjectPageProps> = ({
|
||||
</aside>
|
||||
</div> */}
|
||||
|
||||
<PaymentModal
|
||||
{/* <PaymentModal
|
||||
isOpen={modalOpen}
|
||||
onRequestClose={closeModal}
|
||||
project={selectedProject}
|
||||
/>
|
||||
/> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { NextPage } from 'next'
|
||||
import Head from 'next/head'
|
||||
import { useEffect, useState } from 'react'
|
||||
import PaymentModal from '../../components/PaymentModal'
|
||||
import ProjectCard from '../../components/ProjectCard'
|
||||
import { ProjectItem } from '../../utils/types'
|
||||
import { getAllPosts } from '../../utils/md'
|
||||
@@ -45,11 +44,11 @@ const AllProjects: NextPage<{ projects: ProjectItem[] }> = ({ projects }) => {
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
<PaymentModal
|
||||
{/* <PaymentModal
|
||||
isOpen={modalOpen}
|
||||
onRequestClose={closeModal}
|
||||
project={selectedProject}
|
||||
/>
|
||||
/> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,13 @@ type PasswordResetJwtPayload = {
|
||||
|
||||
export const authRouter = router({
|
||||
register: publicProcedure
|
||||
.input(z.object({ email: z.string().email(), password: z.string() }))
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string().trim().min(1),
|
||||
email: z.string().email(),
|
||||
password: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
await authenticateKeycloakClient()
|
||||
|
||||
@@ -36,7 +42,7 @@ export const authRouter = router({
|
||||
{ type: 'password', value: input.password, temporary: false },
|
||||
],
|
||||
requiredActions: ['VERIFY_EMAIL'],
|
||||
attributes: { passwordResetTokenVersion: 1 },
|
||||
attributes: { name: input.name, passwordResetTokenVersion: 1 },
|
||||
enabled: true,
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Stripe } from 'stripe'
|
||||
import { z } from 'zod'
|
||||
import { protectedProcedure, publicProcedure, router } from '../trpc'
|
||||
import { CURRENCY, MIN_AMOUNT } from '../../config'
|
||||
import { CURRENCY, MAX_AMOUNT, MIN_AMOUNT } from '../../config'
|
||||
import { env } from '../../env.mjs'
|
||||
import { btcpayApi, keycloak, prisma } from '../services'
|
||||
import { authenticateKeycloakClient } from '../utils/keycloak'
|
||||
@@ -21,7 +21,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),
|
||||
amount: z.number().min(MIN_AMOUNT).max(MAX_AMOUNT),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -34,7 +34,7 @@ export const donationRouter = router({
|
||||
await authenticateKeycloakClient()
|
||||
const user = await keycloak.users.findOne({ id: userId })!
|
||||
email = user?.email!
|
||||
name = (user?.firstName || '') + ' ' + (user?.lastName || '')
|
||||
name = user?.attributes?.name?.[0]
|
||||
stripeCustomerId = user?.attributes?.stripeCustomerId?.[0] || null
|
||||
}
|
||||
|
||||
@@ -96,7 +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),
|
||||
amount: z.number().min(MIN_AMOUNT).max(MAX_AMOUNT),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -108,7 +108,7 @@ export const donationRouter = router({
|
||||
await authenticateKeycloakClient()
|
||||
const user = await keycloak.users.findOne({ id: userId })
|
||||
email = user?.email!
|
||||
name = (user?.firstName || '') + ' ' + (user?.lastName || '')
|
||||
name = user?.attributes?.name?.[0] || null
|
||||
}
|
||||
|
||||
const metadata: DonationMetadata = {
|
||||
|
||||
Reference in New Issue
Block a user