Donation form fixes and improvements

This commit is contained in:
Artur N
2024-06-25 21:27:21 -03:00
parent 65c8b59a0c
commit 1caf668ef9
15 changed files with 435 additions and 367 deletions

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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: '/' })}>

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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) {

View File

@@ -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
View File

@@ -0,0 +1,7 @@
# Keycloak
List of necessary custom attributes for the Keycloak user:
- `name`
- `passwordResetTokenVersion`
- `stripeCustomerId`

View File

@@ -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 &rarr;
</Link>
</CustomLink>
</div>
</div>
</div>
<PaymentModal
isOpen={modalOpen}
onRequestClose={closeModal}
project={selectedProject}
/>
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogContent>
<DonationFormModal project={generalFund} />
</DialogContent>
</Dialog>
</>
)
}

View File

@@ -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}
/>
/> */}
</>
)
}

View File

@@ -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}
/>
/> */}
</>
)
}

View File

@@ -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) {

View File

@@ -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 = {