mirror of
https://github.com/MAGICGrants/campaign-site.git
synced 2026-01-08 20:08:05 -05:00
feat: membership flow and UI improvements
This commit is contained in:
@@ -6,11 +6,7 @@ import Link from './CustomLink'
|
||||
import MobileNav from './MobileNav'
|
||||
import { fundHeaderNavLinks } from '../data/headerNavLinks'
|
||||
import MagicLogo from './MagicLogo'
|
||||
import { Dialog, DialogContent, DialogTrigger } from './ui/dialog'
|
||||
import { Button } from './ui/button'
|
||||
import RegisterFormModal from './RegisterFormModal'
|
||||
import LoginFormModal from './LoginFormModal'
|
||||
import PasswordResetFormModal from './PasswordResetFormModal'
|
||||
import { Avatar, AvatarFallback } from './ui/avatar'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -28,25 +24,10 @@ import FiroLogo from './FiroLogo'
|
||||
import PrivacyGuidesLogo from './PrivacyGuidesLogo'
|
||||
|
||||
const Header = () => {
|
||||
const [registerIsOpen, setRegisterIsOpen] = useState(false)
|
||||
const [loginIsOpen, setLoginIsOpen] = useState(false)
|
||||
const [passwordResetIsOpen, setPasswordResetIsOpen] = useState(false)
|
||||
const router = useRouter()
|
||||
const session = useSession()
|
||||
const fundSlug = useFundSlug()
|
||||
|
||||
useEffect(() => {
|
||||
if (router.query.loginEmail) {
|
||||
setLoginIsOpen(true)
|
||||
}
|
||||
}, [router.query.loginEmail])
|
||||
|
||||
useEffect(() => {
|
||||
if (router.query.registerEmail) {
|
||||
setRegisterIsOpen(true)
|
||||
}
|
||||
}, [router.query.registerEmail])
|
||||
|
||||
const fund = fundSlug ? funds[fundSlug] : null
|
||||
|
||||
return (
|
||||
@@ -87,42 +68,23 @@ const Header = () => {
|
||||
|
||||
{!!fund && session.status !== 'authenticated' && (
|
||||
<>
|
||||
<Dialog open={loginIsOpen} onOpenChange={setLoginIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="w-18 block sm:hidden" size="sm">
|
||||
Login
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="w-24 hidden sm:block">
|
||||
Login
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<LoginFormModal
|
||||
close={() => setLoginIsOpen(false)}
|
||||
openRegisterModal={() => setRegisterIsOpen(true)}
|
||||
openPasswordResetModal={() => setPasswordResetIsOpen(true)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Link href={`/${fund.slug}/login`}>
|
||||
<Button variant="outline" className="w-18 block sm:hidden" size="sm">
|
||||
Login
|
||||
</Button>
|
||||
|
||||
<Dialog open={registerIsOpen} onOpenChange={setRegisterIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="w-18 block sm:hidden" size="sm">
|
||||
Register
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="w-24 hidden sm:block">Register</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<RegisterFormModal
|
||||
close={() => setRegisterIsOpen(false)}
|
||||
openLoginModal={() => setLoginIsOpen(true)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button variant="outline" className="w-24 hidden sm:block">
|
||||
Login
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link href={`/${fund.slug}/register`}>
|
||||
<Button className="w-18 block sm:hidden" size="sm">
|
||||
Register
|
||||
</Button>
|
||||
|
||||
<Button className="w-24 hidden sm:block">Register</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -164,12 +126,6 @@ const Header = () => {
|
||||
|
||||
{!!fundSlug && <MobileNav />}
|
||||
</div>
|
||||
|
||||
<Dialog open={passwordResetIsOpen} onOpenChange={setPasswordResetIsOpen}>
|
||||
<DialogContent>
|
||||
<PasswordResetFormModal close={() => setPasswordResetIsOpen(false)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@ const LayoutWrapper = ({ children }: Props) => {
|
||||
useEffect(() => {
|
||||
if (session?.error === 'RefreshAccessTokenError') {
|
||||
if (fundSlug) {
|
||||
signOut({ callbackUrl: `/${fundSlug}/?loginEmail=${session?.user.email}` })
|
||||
signOut({
|
||||
callbackUrl: `/${fundSlug}/login?email=${encodeURIComponent(session?.user.email)}`,
|
||||
})
|
||||
} else {
|
||||
signOut({ callbackUrl: '/' })
|
||||
}
|
||||
@@ -38,7 +40,7 @@ const LayoutWrapper = ({ children }: Props) => {
|
||||
<SectionContainer>
|
||||
<div className="flex h-screen flex-col justify-between">
|
||||
<Header />
|
||||
<main className="grow">{children}</main>
|
||||
<main className="flex flex-col items-start grow">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</SectionContainer>
|
||||
|
||||
@@ -17,7 +17,7 @@ const PerkCard: React.FC<Props> = ({ perk }) => {
|
||||
<Link href={`/${fundSlug}/perks/${perk.documentId}`}>
|
||||
<figure
|
||||
className={cn(
|
||||
'max-w-sm min-h-[360px] h-full space-y-2 flex flex-col rounded-xl border-b-4 bg-white cursor-pointer',
|
||||
'max-w-sm min-h-[360px] h-full space-y-2 flex flex-col rounded-lg border-b-4 bg-white cursor-pointer',
|
||||
fundSlug === 'monero' && 'border-monero',
|
||||
fundSlug === 'firo' && 'border-firo',
|
||||
fundSlug === 'privacyguides' && 'border-privacyguides',
|
||||
|
||||
@@ -1,696 +0,0 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import { BoxIcon, Check, ChevronsUpDown, ShoppingBagIcon, TruckIcon } from 'lucide-react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { StrapiPerkPopulated } from '../server/types'
|
||||
import { env } from '../env.mjs'
|
||||
import { Button } from './ui/button'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from './ui/form'
|
||||
import { Input } from './ui/input'
|
||||
import { toast } from './ui/use-toast'
|
||||
import { useFundSlug } from '../utils/use-fund-slug'
|
||||
import { trpc } from '../utils/trpc'
|
||||
import Spinner from './Spinner'
|
||||
import { cn } from '../utils/cn'
|
||||
import { Label } from './ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from './ui/command'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table'
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
} from './ui/carousel'
|
||||
import CustomLink from './CustomLink'
|
||||
import { Checkbox } from './ui/checkbox'
|
||||
|
||||
type Props = { perk: StrapiPerkPopulated; balance: number; close: () => void }
|
||||
|
||||
const pointFormat = Intl.NumberFormat('en', { notation: 'standard', compactDisplay: 'long' })
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
shippingAddressLine1: z.string().min(1),
|
||||
shippingAddressLine2: z.string(),
|
||||
shippingCity: z.string().min(1),
|
||||
shippingState: z.string(),
|
||||
shippingCountry: z.string().min(1),
|
||||
shippingZip: z.string().min(1),
|
||||
shippingPhone: z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(/^\+?\d{6,15}$/, 'Invalid phone number.'),
|
||||
shippingTaxNumber: z.string(),
|
||||
printfulSyncVariantId: z.string().optional(),
|
||||
_shippingStateOptionsLength: z.number(),
|
||||
_useAccountMailingAddress: z.boolean(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
const cpfRegex =
|
||||
/([0-9]{2}[\.]?[0-9]{3}[\.]?[0-9]{3}[\/]?[0-9]{4}[-]?[0-9]{2})|([0-9]{3}[\.]?[0-9]{3}[\.]?[0-9]{3}[-]?[0-9]{2})/
|
||||
|
||||
if (data.shippingCountry === 'BR') {
|
||||
if (data.shippingTaxNumber.length < 1) {
|
||||
ctx.addIssue({
|
||||
path: ['shippingTaxNumber'],
|
||||
code: 'custom',
|
||||
message: 'CPF is required.',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!cpfRegex.test(data.shippingTaxNumber)) {
|
||||
ctx.addIssue({
|
||||
path: ['shippingTaxNumber'],
|
||||
code: 'custom',
|
||||
message: 'Invalid CPF.',
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (!data.shippingState && data._shippingStateOptionsLength) {
|
||||
ctx.addIssue({
|
||||
path: ['shippingState'],
|
||||
code: 'custom',
|
||||
message: 'State is required.',
|
||||
})
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
type PerkPurchaseInputs = z.infer<typeof schema>
|
||||
|
||||
type CostEstimate = { product: number; shipping: number; tax: number; total: number }
|
||||
|
||||
function PerkPurchaseFormModal({ perk, balance, close }: Props) {
|
||||
const router = useRouter()
|
||||
const fundSlug = useFundSlug()
|
||||
const getCountriesQuery = trpc.perk.getCountries.useQuery()
|
||||
const getUserAttributesQuery = trpc.account.getUserAttributes.useQuery()
|
||||
const purchasePerkMutation = trpc.perk.purchasePerk.useMutation()
|
||||
const estimatePrintfulOrderCosts = trpc.perk.estimatePrintfulOrderCosts.useMutation()
|
||||
|
||||
const getPrintfulProductVariantsQuery = trpc.perk.getPrintfulProductVariants.useQuery(
|
||||
{ printfulProductId: perk.printfulProductId || '' },
|
||||
{ enabled: !!perk.printfulProductId }
|
||||
)
|
||||
|
||||
const form = useForm<PerkPurchaseInputs>({
|
||||
resolver: zodResolver(perk.needsShippingAddress ? schema : z.object({})),
|
||||
mode: 'all',
|
||||
defaultValues: {
|
||||
_shippingStateOptionsLength: 0,
|
||||
shippingAddressLine1: '',
|
||||
shippingAddressLine2: '',
|
||||
shippingCity: '',
|
||||
shippingState: '',
|
||||
shippingCountry: '',
|
||||
shippingZip: '',
|
||||
shippingPhone: '',
|
||||
shippingTaxNumber: '',
|
||||
},
|
||||
shouldFocusError: false,
|
||||
})
|
||||
|
||||
const [countrySelectOpen, setCountrySelectOpen] = useState(false)
|
||||
const [stateSelectOpen, setStateSelectOpen] = useState(false)
|
||||
const [costEstimate, setCostEstimate] = useState<CostEstimate | null>(null)
|
||||
|
||||
const hasEnoughBalance = balance - (costEstimate?.total || perk.price) > 0
|
||||
|
||||
const shippingCountryOptions = (getCountriesQuery.data || []).map((country) => ({
|
||||
label: country.name,
|
||||
value: country.code,
|
||||
}))
|
||||
|
||||
const shippingCountry = form.watch('shippingCountry')
|
||||
const shippingState = form.watch('shippingState')
|
||||
const printfulSyncVariantId = form.watch('printfulSyncVariantId')
|
||||
const useAccountMailingAddress = form.watch('_useAccountMailingAddress')
|
||||
|
||||
const shippingStateOptions = useMemo(() => {
|
||||
const selectedCountry = (getCountriesQuery.data || []).find(
|
||||
(country) => country.code === shippingCountry
|
||||
)
|
||||
|
||||
const stateOptions =
|
||||
selectedCountry?.states?.map((state) => ({
|
||||
label: state.name,
|
||||
value: state.code,
|
||||
})) || []
|
||||
|
||||
return stateOptions
|
||||
}, [shippingCountry])
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue('shippingState', '')
|
||||
form.setValue('shippingTaxNumber', '')
|
||||
}, [shippingCountry])
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue('_shippingStateOptionsLength', shippingStateOptions.length)
|
||||
}, [shippingStateOptions])
|
||||
|
||||
useEffect(() => {
|
||||
if (!getUserAttributesQuery.data) return
|
||||
|
||||
if (useAccountMailingAddress) {
|
||||
form.setValue('shippingAddressLine1', getUserAttributesQuery.data.addressLine1)
|
||||
form.setValue('shippingAddressLine2', getUserAttributesQuery.data.addressLine2)
|
||||
form.setValue('shippingCountry', getUserAttributesQuery.data.addressCountry)
|
||||
form.setValue('shippingCity', getUserAttributesQuery.data.addressCity)
|
||||
form.setValue('shippingZip', getUserAttributesQuery.data.addressZip)
|
||||
setTimeout(() => form.setValue('shippingState', getUserAttributesQuery.data.addressState), 20)
|
||||
} else {
|
||||
form.setValue('shippingAddressLine1', '')
|
||||
form.setValue('shippingAddressLine2', '')
|
||||
form.setValue('shippingCountry', '')
|
||||
form.setValue('shippingState', '')
|
||||
form.setValue('shippingCity', '')
|
||||
form.setValue('shippingZip', '')
|
||||
}
|
||||
}, [useAccountMailingAddress])
|
||||
|
||||
async function onSubmit(data: PerkPurchaseInputs) {
|
||||
if (!fundSlug) return
|
||||
|
||||
// Get order estimate if needed
|
||||
if (perk.needsShippingAddress && !costEstimate && data.printfulSyncVariantId) {
|
||||
try {
|
||||
const _costEstimate = await estimatePrintfulOrderCosts.mutateAsync({
|
||||
...data,
|
||||
|
||||
printfulSyncVariantId: Number(data.printfulSyncVariantId),
|
||||
})
|
||||
|
||||
setCostEstimate(_costEstimate)
|
||||
} catch {
|
||||
toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Make purchase
|
||||
if (!perk.needsShippingAddress || !!costEstimate) {
|
||||
try {
|
||||
await purchasePerkMutation.mutateAsync({
|
||||
perkId: perk.documentId,
|
||||
fundSlug,
|
||||
perkPrintfulSyncVariantId: Number(data.printfulSyncVariantId) || undefined,
|
||||
...data,
|
||||
})
|
||||
toast({ title: 'Perk successfully purchased!' })
|
||||
router.push(`/${fundSlug}/account/point-history`)
|
||||
close()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-w-0 flex flex-col md:flex-row gap-8 items-start">
|
||||
<div className="p-10 hidden md:block">
|
||||
<Carousel className="w-80 h-80">
|
||||
<CarouselContent>
|
||||
{perk.images.map((image) => (
|
||||
<CarouselItem key={image.formats.medium.url}>
|
||||
<Image
|
||||
alt={perk.name}
|
||||
src={
|
||||
process.env.NODE_ENV !== 'production'
|
||||
? env.NEXT_PUBLIC_STRAPI_URL + image.formats.medium.url
|
||||
: image.formats.medium.url
|
||||
}
|
||||
width={600}
|
||||
height={600}
|
||||
style={{ objectFit: 'contain' }}
|
||||
className="w-80 h-80"
|
||||
/>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<CarouselPrevious />
|
||||
<CarouselNext />
|
||||
</Carousel>
|
||||
</div>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="w-full md:max-w-md flex flex-col space-y-8"
|
||||
>
|
||||
<div className="flex flex-col justify-start">
|
||||
<div className="mx-auto p-10 md:hidden justify-center items-center">
|
||||
<Carousel className="w-40 sm:w-56 h-40 sm:h-56">
|
||||
<CarouselContent>
|
||||
{perk.images.map((image) => (
|
||||
<CarouselItem key={image.formats.medium.url}>
|
||||
<Image
|
||||
alt={perk.name}
|
||||
src={
|
||||
process.env.NODE_ENV !== 'production'
|
||||
? env.NEXT_PUBLIC_STRAPI_URL + image.formats.medium.url
|
||||
: image.formats.medium.url
|
||||
}
|
||||
width={200}
|
||||
height={200}
|
||||
style={{ objectFit: 'contain' }}
|
||||
className="w-40 sm:w-56 h-40 sm:h-56"
|
||||
/>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<CarouselPrevious />
|
||||
<CarouselNext />
|
||||
</Carousel>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-6">
|
||||
<div className="flex flex-col">
|
||||
<h1 className="font-semibold">{perk.name}</h1>
|
||||
{!costEstimate && <p className="text-muted-foreground">{perk.description}</p>}
|
||||
{!costEstimate && perk.productDetailsUrl && (
|
||||
<CustomLink className="text-xs" href={perk.productDetailsUrl}>
|
||||
View product details
|
||||
</CustomLink>
|
||||
)}
|
||||
{!!costEstimate && printfulSyncVariantId && (
|
||||
<p className="text-muted-foreground">
|
||||
{
|
||||
getPrintfulProductVariantsQuery.data?.find(
|
||||
(variant) => variant.id === Number(printfulSyncVariantId)
|
||||
)?.name
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!costEstimate && (
|
||||
<div className="flex flex-col">
|
||||
<Label>Price</Label>
|
||||
|
||||
<p className="mt-1 text-lg text-green-500">
|
||||
<strong className="font-semibold">{pointFormat.format(perk.price)}</strong>{' '}
|
||||
points
|
||||
</p>
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs',
|
||||
hasEnoughBalance ? 'text-muted-foreground' : 'text-red-500'
|
||||
)}
|
||||
>
|
||||
You have {pointFormat.format(balance)} points
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-4 grow">
|
||||
{perk.needsShippingAddress && hasEnoughBalance && !costEstimate && (
|
||||
<>
|
||||
{perk.needsShippingAddress && !!getPrintfulProductVariantsQuery.data && (
|
||||
<div className="flex flex-col">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="printfulSyncVariantId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Options *</FormLabel>
|
||||
<Select onValueChange={field.onChange}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select size and color" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{getPrintfulProductVariantsQuery.data.map((variant) => (
|
||||
<SelectItem
|
||||
key={variant.id}
|
||||
value={variant.id.toString()}
|
||||
>{`${variant.size} | ${variant.color}`}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{perk.needsShippingAddress && !getPrintfulProductVariantsQuery.data && <Spinner />}
|
||||
|
||||
{getUserAttributesQuery.data?.addressLine1 && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="_useAccountMailingAddress"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<div className="space-y-1 flex flex-col items-start leading-none">
|
||||
<FormLabel>Use saved mailing address</FormLabel>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="shippingAddressLine1"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Address line 1 *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={useAccountMailingAddress} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="shippingAddressLine2"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Address line 2</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={useAccountMailingAddress} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="shippingCountry"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Country *</FormLabel>
|
||||
<Popover
|
||||
modal
|
||||
open={countrySelectOpen}
|
||||
onOpenChange={(open) => setCountrySelectOpen(open)}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<div>
|
||||
<FormControl>
|
||||
<Select
|
||||
open={countrySelectOpen}
|
||||
onValueChange={() => setCountrySelectOpen(false)}
|
||||
disabled={useAccountMailingAddress}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
(getCountriesQuery.data || []).find(
|
||||
(country) => country.code === shippingCountry
|
||||
)?.name || ''
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search country..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No country found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{shippingCountryOptions.map((country) => (
|
||||
<CommandItem
|
||||
value={country.label}
|
||||
key={country.value}
|
||||
onSelect={() => (
|
||||
form.setValue('shippingCountry', country.value, {
|
||||
shouldValidate: true,
|
||||
}),
|
||||
setCountrySelectOpen(false)
|
||||
)}
|
||||
>
|
||||
{country.label}
|
||||
<Check
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
country.value === field.value ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!!shippingStateOptions.length && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="shippingState"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>State *</FormLabel>
|
||||
<Popover
|
||||
modal
|
||||
open={stateSelectOpen}
|
||||
onOpenChange={(open) => setStateSelectOpen(open)}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<div>
|
||||
<FormControl>
|
||||
<Select disabled={useAccountMailingAddress}>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
shippingStateOptions.find(
|
||||
(state) => state.value === shippingState
|
||||
)?.label
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search state..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No state found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{shippingStateOptions.map((state) => (
|
||||
<CommandItem
|
||||
value={state.label}
|
||||
key={state.value}
|
||||
onSelect={() => (
|
||||
form.setValue('shippingState', state.value, {
|
||||
shouldValidate: true,
|
||||
}),
|
||||
setStateSelectOpen(false)
|
||||
)}
|
||||
>
|
||||
{state.label}
|
||||
<Check
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
state.value === field.value ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="shippingCity"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>City *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={useAccountMailingAddress} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="shippingZip"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Postal code *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={useAccountMailingAddress} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="shippingPhone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Phone number *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{shippingCountry === 'BR' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="shippingTaxNumber"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Tax number (CPF) *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Price subject to change depending on your region.
|
||||
</span>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
disabled={
|
||||
!(
|
||||
form.formState.isValid &&
|
||||
hasEnoughBalance &&
|
||||
!estimatePrintfulOrderCosts.isPending
|
||||
)
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
{estimatePrintfulOrderCosts.isPending ? <Spinner /> : <TruckIcon />}
|
||||
Calculate shipping costs
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!!costEstimate && (
|
||||
<div className="flex flex-col mb-auto">
|
||||
<Table className="w-fit">
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">Item</TableCell>
|
||||
<TableCell>{pointFormat.format(costEstimate.product)} points</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">Shipping</TableCell>
|
||||
<TableCell>{pointFormat.format(costEstimate.shipping)} points</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">Tax</TableCell>
|
||||
<TableCell>{pointFormat.format(costEstimate.tax)} points</TableCell>
|
||||
</TableRow>
|
||||
<TableRow className="text-lg">
|
||||
<TableCell className="font-semibold">Total</TableCell>
|
||||
<TableCell className="text-green-500">
|
||||
<strong className="font-semibold">
|
||||
{pointFormat.format(costEstimate.total)}
|
||||
</strong>{' '}
|
||||
points
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs',
|
||||
hasEnoughBalance ? 'text-muted-foreground' : 'text-red-500'
|
||||
)}
|
||||
>
|
||||
You have {pointFormat.format(balance)} points
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{((perk.needsShippingAddress && !!costEstimate) || !perk.needsShippingAddress) && (
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
disabled={
|
||||
!(form.formState.isValid && hasEnoughBalance && !purchasePerkMutation.isPending)
|
||||
}
|
||||
className="w-full max-w-96 mx-auto md:mx-0"
|
||||
>
|
||||
{purchasePerkMutation.isPending ? <Spinner /> : <ShoppingBagIcon />}
|
||||
Purchase
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PerkPurchaseFormModal
|
||||
@@ -33,7 +33,7 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project, customImageStyles })
|
||||
<Link href={`/${project.fund}/projects/${project.slug}`} passHref target="_blank">
|
||||
<figure
|
||||
className={cn(
|
||||
'max-w-sm min-h-[460px] h-full space-y-2 flex flex-col rounded-xl border-b-4 bg-white',
|
||||
'max-w-sm min-h-[460px] h-full space-y-2 flex flex-col rounded-lg border-b-4 bg-white',
|
||||
project.fund === 'monero' && 'border-monero',
|
||||
project.fund === 'firo' && 'border-firo',
|
||||
project.fund === 'privacyguides' && 'border-privacyguides',
|
||||
|
||||
@@ -27,7 +27,7 @@ const toastVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'border bg-background text-foreground',
|
||||
default: 'border bg-white text-foreground',
|
||||
destructive:
|
||||
'destructive group border-destructive bg-destructive text-destructive-foreground',
|
||||
},
|
||||
@@ -40,8 +40,7 @@ const toastVariants = cva(
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
|
||||
@@ -24,4 +24,4 @@ export default withAuth({
|
||||
},
|
||||
})
|
||||
|
||||
export const config = { matcher: ['/:path/account/:path*', '/:path/membership/:path*'] }
|
||||
export const config = { matcher: ['/:path/account/:path*'] }
|
||||
|
||||
@@ -171,10 +171,11 @@ function Settings() {
|
||||
async function onChangeProfileSubmit(data: ChangeProfileFormInputs) {
|
||||
try {
|
||||
await changeProfileMutation.mutateAsync(data)
|
||||
toast({ title: 'Your profile has successfully been changed!' })
|
||||
toast({ title: 'Success', description: 'Your profile has successfully been changed!' })
|
||||
} catch (error) {
|
||||
return toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
title: 'Error',
|
||||
description: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -186,7 +187,7 @@ function Settings() {
|
||||
try {
|
||||
await requestEmailChangeMutation.mutateAsync({ fundSlug, newEmail: data.newEmail })
|
||||
changeEmailForm.reset()
|
||||
toast({ title: 'A verification link has been sent to your email.' })
|
||||
toast({ title: 'Success', description: 'A verification link has been sent to your email.' })
|
||||
} catch (error) {
|
||||
const errorMessage = (error as any).message
|
||||
|
||||
@@ -199,7 +200,8 @@ function Settings() {
|
||||
}
|
||||
|
||||
return toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
title: 'Error',
|
||||
description: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -214,9 +216,12 @@ function Settings() {
|
||||
|
||||
changePasswordForm.reset()
|
||||
|
||||
toast({ title: 'Your password has successfully been changed! Please log in again.' })
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Your password has successfully been changed! Please log in again.',
|
||||
})
|
||||
await signOut({ redirect: false })
|
||||
router.push(`/${fundSlug}/?loginEmail=${session.data?.user.email}`)
|
||||
router.push(`/${fundSlug}/login?email=${encodeURIComponent(session.data?.user.email!)}`)
|
||||
} catch (error) {
|
||||
const errorMessage = (error as any).message
|
||||
|
||||
@@ -229,7 +234,8 @@ function Settings() {
|
||||
}
|
||||
|
||||
return toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
title: 'Error',
|
||||
description: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -238,10 +244,14 @@ function Settings() {
|
||||
async function onChangeMailingAddressSubmit(data: ChangeMailingAddressFormInputs) {
|
||||
try {
|
||||
await changeMailingAddressMutation.mutateAsync(data)
|
||||
toast({ title: 'Your mailing address has successfully been changed!' })
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Your mailing address has successfully been changed!',
|
||||
})
|
||||
} catch (error) {
|
||||
return toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
title: 'Error',
|
||||
description: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -109,10 +109,7 @@ function DonationPage({ fund: fundSlug, slug, project }: Props) {
|
||||
|
||||
window.location.assign(result.url)
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
toast({ title: 'Error', description: 'Sorry, something went wrong.', variant: 'destructive' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,10 +134,7 @@ function DonationPage({ fund: fundSlug, slug, project }: Props) {
|
||||
|
||||
window.location.assign(result.url)
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
toast({ title: 'Error', description: 'Sorry, something went wrong.', variant: 'destructive' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +150,7 @@ function DonationPage({ fund: fundSlug, slug, project }: Props) {
|
||||
<Head>
|
||||
<title>Donate to {project.title}</title>
|
||||
</Head>
|
||||
<div className="max-w-[540px] mx-auto p-6 space-y-6 rounded-xl bg-white">
|
||||
<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
|
||||
@@ -165,7 +159,7 @@ function DonationPage({ fund: fundSlug, slug, project }: Props) {
|
||||
width={200}
|
||||
height={96}
|
||||
objectFit="cover"
|
||||
className="w-36 rounded-xl"
|
||||
className="w-36 rounded-lg"
|
||||
/>
|
||||
<div className="flex flex-col justify-center">
|
||||
<h2 className="text-center sm:text-left font-semibold">Donate to {project.title}</h2>
|
||||
@@ -256,7 +250,9 @@ function DonationPage({ fund: fundSlug, slug, project }: Props) {
|
||||
name="taxDeductible"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<FormLabel>Do you want this donation to potentially qualify for a tax deduction? (US only)</FormLabel>
|
||||
<FormLabel>
|
||||
Do you want this donation to potentially qualify for a tax deduction? (US only)
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
@@ -413,7 +409,7 @@ function DonationPage({ fund: fundSlug, slug, project }: Props) {
|
||||
<div className="flex flex-col items-center">
|
||||
<p className="text-sm">Want to support more projects and receive optional perks?</p>
|
||||
|
||||
<Link href={`/${encodeURIComponent(fundSlug)}/?registerEmail=1`}>
|
||||
<Link href={`/${encodeURIComponent(fundSlug)}/register`}>
|
||||
<Button type="button" size="lg" variant="link">
|
||||
Create an account
|
||||
</Button>
|
||||
|
||||
@@ -2,16 +2,25 @@ import { useRef } from 'react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { GetServerSidePropsContext } from 'next'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { DialogDescription, DialogHeader, DialogTitle } from './ui/dialog'
|
||||
import { Input } from './ui/input'
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from './ui/form'
|
||||
import { Button } from './ui/button'
|
||||
import { useToast } from './ui/use-toast'
|
||||
import { trpc } from '../utils/trpc'
|
||||
import Spinner from './Spinner'
|
||||
import { env } from '../env.mjs'
|
||||
import { Input } from '../../components/ui/input'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '../../components/ui/form'
|
||||
import { Button } from '../../components/ui/button'
|
||||
import { useToast } from '../../components/ui/use-toast'
|
||||
import { trpc } from '../../utils/trpc'
|
||||
import Spinner from '../../components/Spinner'
|
||||
import { env } from '../../env.mjs'
|
||||
import { authOptions } from '../api/auth/[...nextauth]'
|
||||
|
||||
const schema = z.object({
|
||||
turnstileToken: z.string().min(1),
|
||||
@@ -20,9 +29,7 @@ const schema = z.object({
|
||||
|
||||
type PasswordResetFormInputs = z.infer<typeof schema>
|
||||
|
||||
type Props = { close: () => void }
|
||||
|
||||
function PasswordResetFormModal({ close }: Props) {
|
||||
function ForgotPassword() {
|
||||
const { toast } = useToast()
|
||||
const turnstileRef = useRef<TurnstileInstance | null>()
|
||||
|
||||
@@ -34,22 +41,18 @@ function PasswordResetFormModal({ close }: Props) {
|
||||
try {
|
||||
await requestPasswordResetMutation.mutateAsync(data)
|
||||
|
||||
toast({ title: 'A password reset link has been sent to your email.' })
|
||||
close()
|
||||
toast({ title: 'Success', description: 'A password reset link has been sent to your email.' })
|
||||
form.reset({ email: '' })
|
||||
} catch (error) {
|
||||
toast({ title: 'Sorry, something went wrong.', variant: 'destructive' })
|
||||
toast({ title: 'Error', description: 'Sorry, something went wrong.', variant: 'destructive' })
|
||||
}
|
||||
|
||||
turnstileRef.current?.reset()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Reset Password</DialogTitle>
|
||||
<DialogDescription>Recover your account.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="w-full max-w-xl m-auto p-6 flex flex-col space-y-4 bg-white rounded-lg">
|
||||
<h1 className="font-bold">Request password reset</h1>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col space-y-4">
|
||||
@@ -80,8 +83,16 @@ function PasswordResetFormModal({ close }: Props) {
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PasswordResetFormModal
|
||||
export default ForgotPassword
|
||||
|
||||
export async function getServerSideProps({ params, req, res }: GetServerSidePropsContext) {
|
||||
const session = await getServerSession(req, res, authOptions)
|
||||
|
||||
if (session) {
|
||||
return { redirect: { destination: `/${params?.fund!}` } }
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,28 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { signIn, useSession } from 'next-auth/react'
|
||||
import { signIn } from 'next-auth/react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { GetServerSidePropsContext } from 'next'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from './ui/dialog'
|
||||
import { Input } from './ui/input'
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from './ui/form'
|
||||
import { Button } from './ui/button'
|
||||
import { useToast } from './ui/use-toast'
|
||||
import { useFundSlug } from '../utils/use-fund-slug'
|
||||
import Spinner from './Spinner'
|
||||
import { env } from '../env.mjs'
|
||||
import { Input } from '../../components/ui/input'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '../../components/ui/form'
|
||||
import { Button } from '../../components/ui/button'
|
||||
import { useToast } from '../../components/ui/use-toast'
|
||||
import { useFundSlug } from '../../utils/use-fund-slug'
|
||||
import Spinner from '../../components/Spinner'
|
||||
import { env } from '../../env.mjs'
|
||||
import { authOptions } from '../api/auth/[...nextauth]'
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email(),
|
||||
@@ -23,13 +32,7 @@ const schema = z.object({
|
||||
|
||||
type LoginFormInputs = z.infer<typeof schema>
|
||||
|
||||
type Props = {
|
||||
close: () => void
|
||||
openPasswordResetModal: () => void
|
||||
openRegisterModal: () => void
|
||||
}
|
||||
|
||||
function LoginFormModal({ close, openPasswordResetModal, openRegisterModal }: Props) {
|
||||
function Login() {
|
||||
const { toast } = useToast()
|
||||
const router = useRouter()
|
||||
const fundSlug = useFundSlug()
|
||||
@@ -43,12 +46,11 @@ function LoginFormModal({ close, openPasswordResetModal, openRegisterModal }: Pr
|
||||
|
||||
useEffect(() => {
|
||||
if (!fundSlug) return
|
||||
if (router.query.loginEmail) {
|
||||
form.setValue('email', router.query.loginEmail as string)
|
||||
if (router.query.email) {
|
||||
form.setValue('email', router.query.email as string)
|
||||
setTimeout(() => form.setFocus('password'), 100)
|
||||
router.replace(`/${fundSlug}`)
|
||||
}
|
||||
}, [router.query.loginEmail])
|
||||
}, [router.query.email])
|
||||
|
||||
async function onSubmit(data: LoginFormInputs) {
|
||||
const result = await signIn('credentials', {
|
||||
@@ -69,25 +71,24 @@ function LoginFormModal({ close, openPasswordResetModal, openRegisterModal }: Pr
|
||||
)
|
||||
}
|
||||
|
||||
return toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
toast({ title: 'Error', description: 'Sorry, something went wrong.', variant: 'destructive' })
|
||||
}
|
||||
|
||||
toast({
|
||||
title: 'Successfully logged in!',
|
||||
title: 'Success',
|
||||
description: 'Successfully logged in!',
|
||||
})
|
||||
|
||||
close()
|
||||
if (router.query.nextAction === 'membership') {
|
||||
router.push(`/${fundSlug}/membership`)
|
||||
} else {
|
||||
router.push(`/${fundSlug}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Login</DialogTitle>
|
||||
<DialogDescription>Log into your account.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="w-full max-w-xl m-auto p-6 flex flex-col space-y-4 bg-white rounded-lg">
|
||||
<h1 className="font-bold">Login</h1>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col space-y-4">
|
||||
@@ -122,7 +123,7 @@ function LoginFormModal({ close, openPasswordResetModal, openRegisterModal }: Pr
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => (openPasswordResetModal(), close())}
|
||||
onClick={() => router.push(`/${fundSlug}/forgot-password`)}
|
||||
variant="link"
|
||||
className="self-end"
|
||||
>
|
||||
@@ -143,7 +144,7 @@ function LoginFormModal({ close, openPasswordResetModal, openRegisterModal }: Pr
|
||||
className="grow basis-0"
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={() => (openRegisterModal(), close())}
|
||||
onClick={() => router.push(`/${fundSlug}/register`)}
|
||||
>
|
||||
Register
|
||||
</Button>
|
||||
@@ -158,8 +159,16 @@ function LoginFormModal({ close, openPasswordResetModal, openRegisterModal }: Pr
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginFormModal
|
||||
export default Login
|
||||
|
||||
export async function getServerSideProps({ params, req, res }: GetServerSidePropsContext) {
|
||||
const session = await getServerSession(req, res, authOptions)
|
||||
|
||||
if (session) {
|
||||
return { redirect: { destination: `/${params?.fund!}` } }
|
||||
}
|
||||
}
|
||||
@@ -6,16 +6,17 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { DollarSign } from 'lucide-react'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { FundSlug } from '@prisma/client'
|
||||
import { GetStaticPropsContext } from 'next'
|
||||
import { GetServerSidePropsContext, GetStaticPropsContext } from 'next'
|
||||
import Image from 'next/image'
|
||||
import Head from 'next/head'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { MAX_AMOUNT } from '../../../config'
|
||||
import Spinner from '../../../components/Spinner'
|
||||
import { trpc } from '../../../utils/trpc'
|
||||
import { useToast } from '../../../components/ui/use-toast'
|
||||
import { Button } from '../../../components/ui/button'
|
||||
import { RadioGroup, RadioGroupItem } from '../../../components/ui/radio-group'
|
||||
import { MAX_AMOUNT } from '../../config'
|
||||
import Spinner from '../../components/Spinner'
|
||||
import { trpc } from '../../utils/trpc'
|
||||
import { useToast } from '../../components/ui/use-toast'
|
||||
import { Button } from '../../components/ui/button'
|
||||
import { RadioGroup, RadioGroupItem } from '../../components/ui/radio-group'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -23,18 +24,21 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '../../../components/ui/form'
|
||||
import { Input } from '../../../components/ui/input'
|
||||
import { ProjectItem } from '../../../utils/types'
|
||||
import { getProjectBySlug, getProjects } from '../../../utils/md'
|
||||
import { funds, fundSlugs } from '../../../utils/funds'
|
||||
import Head from 'next/head'
|
||||
} from '../../components/ui/form'
|
||||
import { Input } from '../../components/ui/input'
|
||||
import { ProjectItem } from '../../utils/types'
|
||||
import { funds, fundSlugs } from '../../utils/funds'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '../api/auth/[...nextauth]'
|
||||
import { useEffect } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
type QueryParams = { fund: FundSlug; slug: string }
|
||||
type Props = { project: ProjectItem } & QueryParams
|
||||
|
||||
function MembershipPage({ fund: fundSlug, project }: Props) {
|
||||
const session = useSession()
|
||||
const router = useRouter()
|
||||
const isAuthed = session.status === 'authenticated'
|
||||
|
||||
const schema = z
|
||||
@@ -95,10 +99,7 @@ function MembershipPage({ fund: fundSlug, project }: Props) {
|
||||
|
||||
window.location.assign(result.url)
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
toast({ title: 'Error', description: 'Sorry, something went wrong.', variant: 'destructive' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,21 +122,24 @@ function MembershipPage({ fund: fundSlug, project }: Props) {
|
||||
|
||||
window.location.assign(result.url)
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
toast({ title: 'Error', description: 'Sorry, something went wrong.', variant: 'destructive' })
|
||||
}
|
||||
}
|
||||
|
||||
if (!project) return <></>
|
||||
useEffect(() => {
|
||||
if (session.status === 'unauthenticated') {
|
||||
router.push(`/${fundSlug}/login?nextAction=membership`)
|
||||
}
|
||||
}, [session])
|
||||
|
||||
if (!project || session.status === 'loading') return <></>
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Membership to {project.title}</title>
|
||||
</Head>
|
||||
<div className="max-w-[540px] mx-auto p-6 space-y-6 rounded-xl bg-white">
|
||||
<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
|
||||
@@ -144,7 +148,7 @@ function MembershipPage({ fund: fundSlug, project }: Props) {
|
||||
width={200}
|
||||
height={96}
|
||||
objectFit="cover"
|
||||
className="w-36 rounded-xl"
|
||||
className="w-36 rounded-lg"
|
||||
/>
|
||||
<div className="flex flex-col justify-center">
|
||||
<h2 className="text-center sm:text-left font-semibold">
|
||||
@@ -273,18 +277,18 @@ function MembershipPage({ fund: fundSlug, project }: Props) {
|
||||
defaultValue={field.value}
|
||||
className="flex flex-row space-x-4 text-gray-700"
|
||||
>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="yes" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">Yes</FormLabel>
|
||||
</FormItem>
|
||||
<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 />
|
||||
@@ -375,23 +379,12 @@ function MembershipPage({ fund: fundSlug, project }: Props) {
|
||||
export default MembershipPage
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const projects = await getProjects()
|
||||
|
||||
return {
|
||||
paths: [
|
||||
...fundSlugs.map((fund) => `/${fund}/membership/${fund}`),
|
||||
...projects.map((project) => `/${project.fund}/membership/${project.slug}`),
|
||||
],
|
||||
paths: fundSlugs.map((fund) => `/${fund}/membership`),
|
||||
fallback: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function getStaticProps({ params }: GetStaticPropsContext<QueryParams>) {
|
||||
if (params?.fund === params?.slug && params?.fund) {
|
||||
return { props: { ...params, project: funds[params.fund] } }
|
||||
}
|
||||
|
||||
const project = getProjectBySlug(params?.slug!, params?.fund!)
|
||||
|
||||
return { props: { ...params, project } }
|
||||
return { props: { ...params, project: funds[params?.fund!] } }
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import Head from 'next/head'
|
||||
import Image from 'next/image'
|
||||
import { BoxIcon, Check, ChevronsUpDown, ShoppingBagIcon, TruckIcon } from 'lucide-react'
|
||||
import { Check, ChevronsUpDown, ShoppingBagIcon, TruckIcon } from 'lucide-react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useForm } from 'react-hook-form'
|
||||
@@ -67,7 +68,6 @@ import { GetServerSidePropsContext } from 'next'
|
||||
import { getUserPointBalance } from '../../../server/utils/perks'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '../../api/auth/[...nextauth]'
|
||||
import Head from 'next/head'
|
||||
|
||||
type Props = { perk: StrapiPerkPopulated; balance: number }
|
||||
|
||||
@@ -233,7 +233,8 @@ function Perk({ perk, balance }: Props) {
|
||||
setCostEstimate(_costEstimate)
|
||||
} catch {
|
||||
toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
title: 'Error',
|
||||
description: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -250,12 +251,12 @@ function Perk({ perk, balance }: Props) {
|
||||
perkPrintfulSyncVariantId: Number(data.printfulSyncVariantId) || undefined,
|
||||
...data,
|
||||
})
|
||||
toast({ title: 'Perk successfully purchased!' })
|
||||
toast({ title: 'Success', description: 'Perk successfully purchased!' })
|
||||
router.push(`/${fundSlug}/account/point-history`)
|
||||
close()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
title: 'Error',
|
||||
description: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -297,7 +298,7 @@ function Perk({ perk, balance }: Props) {
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="w-full md:max-w-md p-6 flex flex-col space-y-6 bg-white rounded-xl"
|
||||
className="w-full md:max-w-md p-6 flex flex-col space-y-6 bg-white rounded-lg"
|
||||
>
|
||||
<div className="flex flex-col justify-start">
|
||||
<div className="mx-auto p-10 md:hidden justify-center items-center">
|
||||
|
||||
@@ -16,21 +16,13 @@ import Progress from '../../../components/Progress'
|
||||
import { prisma } from '../../../server/services'
|
||||
import { Button } from '../../../components/ui/button'
|
||||
import { Dialog, DialogContent } from '../../../components/ui/dialog'
|
||||
import LoginFormModal from '../../../components/LoginFormModal'
|
||||
import RegisterFormModal from '../../../components/RegisterFormModal'
|
||||
import PasswordResetFormModal from '../../../components/PasswordResetFormModal'
|
||||
import CustomLink from '../../../components/CustomLink'
|
||||
import { trpc } from '../../../utils/trpc'
|
||||
import { getFundSlugFromUrlPath } from '../../../utils/funds'
|
||||
import { useFundSlug } from '../../../utils/use-fund-slug'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '../../../components/ui/table'
|
||||
import { Table, TableBody, TableCell, TableRow } from '../../../components/ui/table'
|
||||
import { cn } from '../../../utils/cn'
|
||||
|
||||
type SingleProjectPageProps = {
|
||||
@@ -104,7 +96,7 @@ const Project: NextPage<SingleProjectPageProps> = ({ project, donationStats }) =
|
||||
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-xl">
|
||||
<div className="w-full max-w-96 space-y-8 p-6 bg-white rounded-lg">
|
||||
{!project.isFunded && (
|
||||
<div className="w-full">
|
||||
<div className="flex flex-col space-y-2">
|
||||
@@ -181,7 +173,7 @@ const Project: NextPage<SingleProjectPageProps> = ({ project, donationStats }) =
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-96 min-h-72 space-y-4 p-6 bg-white rounded-xl">
|
||||
<div className="w-full max-w-96 min-h-72 space-y-4 p-6 bg-white rounded-lg">
|
||||
<h1 className="font-bold">Leaderboard</h1>
|
||||
|
||||
{leaderboardQuery.data?.length ? (
|
||||
@@ -216,40 +208,11 @@ const Project: NextPage<SingleProjectPageProps> = ({ project, donationStats }) =
|
||||
</div>
|
||||
|
||||
<article
|
||||
className="prose max-w-none mt-4 p-6 xl:col-span-2 bg-white rounded-xl"
|
||||
className="prose max-w-none mt-4 p-6 xl:col-span-2 bg-white rounded-lg"
|
||||
dangerouslySetInnerHTML={{ __html: xss(content || '') }}
|
||||
/>
|
||||
</PageHeading>
|
||||
</div>
|
||||
|
||||
{session.status !== 'authenticated' && (
|
||||
<>
|
||||
<Dialog open={loginIsOpen} onOpenChange={setLoginIsOpen}>
|
||||
<DialogContent>
|
||||
<LoginFormModal
|
||||
close={() => setLoginIsOpen(false)}
|
||||
openRegisterModal={() => setRegisterIsOpen(true)}
|
||||
openPasswordResetModal={() => setPasswordResetIsOpen(true)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={registerIsOpen} onOpenChange={setRegisterIsOpen}>
|
||||
<DialogContent>
|
||||
<RegisterFormModal
|
||||
openLoginModal={() => setLoginIsOpen(true)}
|
||||
close={() => setRegisterIsOpen(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={passwordResetIsOpen} onOpenChange={setPasswordResetIsOpen}>
|
||||
<DialogContent>
|
||||
<PasswordResetFormModal close={() => setPasswordResetIsOpen(false)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,16 +4,11 @@ import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile'
|
||||
import { z } from 'zod'
|
||||
import { GetServerSidePropsContext } from 'next'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from './ui/dialog'
|
||||
import { Input } from './ui/input'
|
||||
import { Input } from '../../components/ui/input'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -22,14 +17,14 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from './ui/form'
|
||||
import { Button } from './ui/button'
|
||||
import { useToast } from './ui/use-toast'
|
||||
import { trpc } from '../utils/trpc'
|
||||
import { useFundSlug } from '../utils/use-fund-slug'
|
||||
import Spinner from './Spinner'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover'
|
||||
import { Select, SelectTrigger, SelectValue } from './ui/select'
|
||||
} from '../../components/ui/form'
|
||||
import { Button } from '../../components/ui/button'
|
||||
import { useToast } from '../../components/ui/use-toast'
|
||||
import { trpc } from '../../utils/trpc'
|
||||
import { useFundSlug } from '../../utils/use-fund-slug'
|
||||
import Spinner from '../../components/Spinner'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../../components/ui/popover'
|
||||
import { Select, SelectTrigger, SelectValue } from '../../components/ui/select'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -37,11 +32,11 @@ import {
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from './ui/command'
|
||||
import { cn } from '../utils/cn'
|
||||
import { Checkbox } from './ui/checkbox'
|
||||
import { env } from '../env.mjs'
|
||||
import { useRouter } from 'next/router'
|
||||
} from '../../components/ui/command'
|
||||
import { cn } from '../../utils/cn'
|
||||
import { Checkbox } from '../../components/ui/checkbox'
|
||||
import { env } from '../../env.mjs'
|
||||
import { authOptions } from '../api/auth/[...nextauth]'
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
@@ -123,9 +118,7 @@ const schema = z
|
||||
|
||||
type RegisterFormInputs = z.infer<typeof schema>
|
||||
|
||||
type Props = { close: () => void; openLoginModal: () => void }
|
||||
|
||||
function RegisterFormModal({ close, openLoginModal }: Props) {
|
||||
function RegisterFormModal() {
|
||||
const router = useRouter()
|
||||
const { toast } = useToast()
|
||||
const fundSlug = useFundSlug()
|
||||
@@ -179,17 +172,6 @@ function RegisterFormModal({ close, openLoginModal }: Props) {
|
||||
return stateOptions
|
||||
}, [addressCountry])
|
||||
|
||||
useEffect(() => {
|
||||
if (!fundSlug) return
|
||||
if (router.query.registerEmail) {
|
||||
if (router.query.registerEmail !== '1') {
|
||||
form.setValue('email', router.query.registerEmail as string)
|
||||
setTimeout(() => form.setFocus('password'), 100)
|
||||
}
|
||||
router.replace(`/${fundSlug}`)
|
||||
}
|
||||
}, [router.query.registerEmail])
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue('address.state', '')
|
||||
}, [addressCountry])
|
||||
@@ -215,13 +197,18 @@ function RegisterFormModal({ close, openLoginModal }: Props) {
|
||||
if (!fundSlug) return
|
||||
|
||||
try {
|
||||
await registerMutation.mutateAsync({ ...data, fundSlug })
|
||||
|
||||
toast({
|
||||
title: 'Please check your email to verify your account.',
|
||||
await registerMutation.mutateAsync({
|
||||
...data,
|
||||
fundSlug,
|
||||
nextAction: router.query.nextAction === 'membership' ? 'membership' : undefined,
|
||||
})
|
||||
|
||||
close()
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Please check your email to verify your account.',
|
||||
})
|
||||
|
||||
router.push(`/${fundSlug}`)
|
||||
} catch (error) {
|
||||
const errorMessage = (error as any).message
|
||||
|
||||
@@ -229,21 +216,15 @@ function RegisterFormModal({ close, openLoginModal }: Props) {
|
||||
return form.setError('email', { message: 'Email is already taken.' }, { shouldFocus: true })
|
||||
}
|
||||
|
||||
toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
toast({ title: 'Error', description: 'Sorry, something went wrong.', variant: 'destructive' })
|
||||
}
|
||||
|
||||
turnstileRef.current?.reset()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Register</DialogTitle>
|
||||
<DialogDescription>Start supporting projects today!</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="w-full max-w-xl m-auto p-6 flex flex-col space-y-4 bg-white rounded-lg">
|
||||
<h1 className="font-bold">Register</h1>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col space-y-4">
|
||||
@@ -557,7 +538,7 @@ function RegisterFormModal({ close, openLoginModal }: Props) {
|
||||
type="button"
|
||||
variant="link"
|
||||
className="grow basis-0"
|
||||
onClick={() => (openLoginModal(), close())}
|
||||
onClick={() => router.push(`/${fundSlug}/login`)}
|
||||
>
|
||||
I already have an account
|
||||
</Button>
|
||||
@@ -572,9 +553,16 @@ function RegisterFormModal({ close, openLoginModal }: Props) {
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
1
|
||||
|
||||
export default RegisterFormModal
|
||||
|
||||
export async function getServerSideProps({ params, req, res }: GetServerSidePropsContext) {
|
||||
const session = await getServerSession(req, res, authOptions)
|
||||
|
||||
if (session) {
|
||||
return { redirect: { destination: `/${params?.fund!}` } }
|
||||
}
|
||||
}
|
||||
@@ -54,14 +54,22 @@ function ResetPassword() {
|
||||
password: data.password,
|
||||
})
|
||||
|
||||
toast({ title: 'Password successfully reset. You may now log in.' })
|
||||
router.push(`/${fundSlug}/?loginEmail=${encodeURIComponent(data.email)}`)
|
||||
toast({ title: 'Success', description: 'Password successfully reset. You may now log in.' })
|
||||
|
||||
if (router.query.nextAction === 'membership') {
|
||||
router.push(
|
||||
`/${fundSlug}/login?email=${encodeURIComponent(data.email)}&nextAction=membership`
|
||||
)
|
||||
} else {
|
||||
router.push(`/${fundSlug}/login?email=${encodeURIComponent(data.email)}`)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = (error as any).message
|
||||
|
||||
if (errorMessage === 'INVALID_TOKEN') {
|
||||
toast({
|
||||
title: 'Invalid password reset link.',
|
||||
title: 'Error',
|
||||
description: 'Invalid password reset link.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
|
||||
@@ -70,10 +78,7 @@ function ResetPassword() {
|
||||
return
|
||||
}
|
||||
|
||||
toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
toast({ title: 'Error', description: 'Sorry, something went wrong.', variant: 'destructive' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,18 @@ function VerifyEmail() {
|
||||
|
||||
try {
|
||||
const result = await verifyEmailMutation.mutateAsync({ token: token as string })
|
||||
toast({ title: 'Email verified! You may now log in.' })
|
||||
toast({ title: 'Success', description: 'Email verified! You may now log in.' })
|
||||
await signOut({ redirect: false })
|
||||
router.push(`/${fundSlug}/?loginEmail=${result.email}`)
|
||||
|
||||
if (router.query.nextAction === 'membership') {
|
||||
router.push(
|
||||
`/${fundSlug}/login?email=${encodeURIComponent(result.email)}&nextAction=membership`
|
||||
)
|
||||
} else {
|
||||
router.push(`/${fundSlug}/login?email=${encodeURIComponent(result.email)}`)
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: 'Invalid verification link.', variant: 'destructive' })
|
||||
toast({ title: 'Error', description: 'Invalid verification link.', variant: 'destructive' })
|
||||
router.push(`/${fundSlug}`)
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -7,22 +7,13 @@ import { useSession } from 'next-auth/react'
|
||||
import { getProjects } from '../../utils/md'
|
||||
import CustomLink from '../../components/CustomLink'
|
||||
import { Button } from '../../components/ui/button'
|
||||
import { Dialog, DialogContent } from '../../components/ui/dialog'
|
||||
import ProjectList from '../../components/ProjectList'
|
||||
import LoginFormModal from '../../components/LoginFormModal'
|
||||
import RegisterFormModal from '../../components/RegisterFormModal'
|
||||
import PasswordResetFormModal from '../../components/PasswordResetFormModal'
|
||||
import { trpc } from '../../utils/trpc'
|
||||
import { funds } from '../../utils/funds'
|
||||
|
||||
const fund = funds['firo']
|
||||
|
||||
const Home: NextPage<{ projects: any }> = ({ projects }) => {
|
||||
const [donateModalOpen, setDonateModalOpen] = useState(false)
|
||||
const [memberModalOpen, setMemberModalOpen] = useState(false)
|
||||
const [registerIsOpen, setRegisterIsOpen] = useState(false)
|
||||
const [loginIsOpen, setLoginIsOpen] = useState(false)
|
||||
const [passwordResetIsOpen, setPasswordResetIsOpen] = useState(false)
|
||||
const session = useSession()
|
||||
|
||||
const userHasMembershipQuery = trpc.donation.userHasMembership.useQuery(
|
||||
@@ -63,12 +54,14 @@ const Home: NextPage<{ projects: any }> = ({ projects }) => {
|
||||
|
||||
{!userHasMembershipQuery.data && (
|
||||
<>
|
||||
{session.status !== 'authenticated' ? (
|
||||
<Button onClick={() => setRegisterIsOpen(true)} variant="light" size="lg">
|
||||
Get Annual Membership
|
||||
</Button>
|
||||
{session.status === 'authenticated' ? (
|
||||
<Link href={`/${fund.slug}/membership`}>
|
||||
<Button variant="light" size="lg">
|
||||
Get Annual Membership
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<Link href={`/${fund.slug}/membership/${fund.slug}`}>
|
||||
<Link href={`/${fund.slug}/register?nextAction=membership`}>
|
||||
<Button variant="light" size="lg">
|
||||
Get Annual Membership
|
||||
</Button>
|
||||
@@ -108,35 +101,6 @@ const Home: NextPage<{ projects: any }> = ({ projects }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{session.status !== 'authenticated' && (
|
||||
<>
|
||||
<Dialog open={loginIsOpen} onOpenChange={setLoginIsOpen}>
|
||||
<DialogContent>
|
||||
<LoginFormModal
|
||||
close={() => setLoginIsOpen(false)}
|
||||
openRegisterModal={() => setRegisterIsOpen(true)}
|
||||
openPasswordResetModal={() => setPasswordResetIsOpen(true)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={registerIsOpen} onOpenChange={setRegisterIsOpen}>
|
||||
<DialogContent>
|
||||
<RegisterFormModal
|
||||
openLoginModal={() => setLoginIsOpen(true)}
|
||||
close={() => setRegisterIsOpen(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={passwordResetIsOpen} onOpenChange={setPasswordResetIsOpen}>
|
||||
<DialogContent>
|
||||
<PasswordResetFormModal close={() => setPasswordResetIsOpen(false)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,22 +7,13 @@ import { useSession } from 'next-auth/react'
|
||||
import { getProjects } from '../../utils/md'
|
||||
import CustomLink from '../../components/CustomLink'
|
||||
import { Button } from '../../components/ui/button'
|
||||
import { Dialog, DialogContent } from '../../components/ui/dialog'
|
||||
import ProjectList from '../../components/ProjectList'
|
||||
import LoginFormModal from '../../components/LoginFormModal'
|
||||
import RegisterFormModal from '../../components/RegisterFormModal'
|
||||
import PasswordResetFormModal from '../../components/PasswordResetFormModal'
|
||||
import { trpc } from '../../utils/trpc'
|
||||
import { funds } from '../../utils/funds'
|
||||
|
||||
const fund = funds['general']
|
||||
|
||||
const Home: NextPage<{ projects: any }> = ({ projects }) => {
|
||||
const [donateModalOpen, setDonateModalOpen] = useState(false)
|
||||
const [memberModalOpen, setMemberModalOpen] = useState(false)
|
||||
const [registerIsOpen, setRegisterIsOpen] = useState(false)
|
||||
const [loginIsOpen, setLoginIsOpen] = useState(false)
|
||||
const [passwordResetIsOpen, setPasswordResetIsOpen] = useState(false)
|
||||
const session = useSession()
|
||||
|
||||
const userHasMembershipQuery = trpc.donation.userHasMembership.useQuery(
|
||||
@@ -64,12 +55,14 @@ const Home: NextPage<{ projects: any }> = ({ projects }) => {
|
||||
|
||||
{!userHasMembershipQuery.data && (
|
||||
<>
|
||||
{session.status !== 'authenticated' ? (
|
||||
<Button onClick={() => setRegisterIsOpen(true)} variant="light" size="lg">
|
||||
Get Annual Membership
|
||||
</Button>
|
||||
{session.status === 'authenticated' ? (
|
||||
<Link href={`/${fund.slug}/membership`}>
|
||||
<Button variant="light" size="lg">
|
||||
Get Annual Membership
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<Link href={`/${fund.slug}/membership/${fund.slug}`}>
|
||||
<Link href={`/${fund.slug}/register?nextAction=membership`}>
|
||||
<Button variant="light" size="lg">
|
||||
Get Annual Membership
|
||||
</Button>
|
||||
@@ -109,35 +102,6 @@ const Home: NextPage<{ projects: any }> = ({ projects }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{session.status !== 'authenticated' && (
|
||||
<>
|
||||
<Dialog open={loginIsOpen} onOpenChange={setLoginIsOpen}>
|
||||
<DialogContent>
|
||||
<LoginFormModal
|
||||
close={() => setLoginIsOpen(false)}
|
||||
openRegisterModal={() => setRegisterIsOpen(true)}
|
||||
openPasswordResetModal={() => setPasswordResetIsOpen(true)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={registerIsOpen} onOpenChange={setRegisterIsOpen}>
|
||||
<DialogContent>
|
||||
<RegisterFormModal
|
||||
openLoginModal={() => setLoginIsOpen(true)}
|
||||
close={() => setRegisterIsOpen(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={passwordResetIsOpen} onOpenChange={setPasswordResetIsOpen}>
|
||||
<DialogContent>
|
||||
<PasswordResetFormModal close={() => setPasswordResetIsOpen(false)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,22 +7,13 @@ import { useSession } from 'next-auth/react'
|
||||
import { getProjects } from '../../utils/md'
|
||||
import CustomLink from '../../components/CustomLink'
|
||||
import { Button } from '../../components/ui/button'
|
||||
import { Dialog, DialogContent } from '../../components/ui/dialog'
|
||||
import ProjectList from '../../components/ProjectList'
|
||||
import LoginFormModal from '../../components/LoginFormModal'
|
||||
import RegisterFormModal from '../../components/RegisterFormModal'
|
||||
import PasswordResetFormModal from '../../components/PasswordResetFormModal'
|
||||
import { trpc } from '../../utils/trpc'
|
||||
import { funds } from '../../utils/funds'
|
||||
|
||||
const fund = funds['monero']
|
||||
|
||||
const Home: NextPage<{ projects: any }> = ({ projects }) => {
|
||||
const [donateModalOpen, setDonateModalOpen] = useState(false)
|
||||
const [memberModalOpen, setMemberModalOpen] = useState(false)
|
||||
const [registerIsOpen, setRegisterIsOpen] = useState(false)
|
||||
const [loginIsOpen, setLoginIsOpen] = useState(false)
|
||||
const [passwordResetIsOpen, setPasswordResetIsOpen] = useState(false)
|
||||
const session = useSession()
|
||||
|
||||
const userHasMembershipQuery = trpc.donation.userHasMembership.useQuery(
|
||||
@@ -63,12 +54,14 @@ const Home: NextPage<{ projects: any }> = ({ projects }) => {
|
||||
|
||||
{!userHasMembershipQuery.data && (
|
||||
<>
|
||||
{session.status !== 'authenticated' ? (
|
||||
<Button onClick={() => setRegisterIsOpen(true)} variant="light" size="lg">
|
||||
Get Annual Membership
|
||||
</Button>
|
||||
{session.status === 'authenticated' ? (
|
||||
<Link href={`/${fund.slug}/membership`}>
|
||||
<Button variant="light" size="lg">
|
||||
Get Annual Membership
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<Link href={`/${fund.slug}/membership/${fund.slug}`}>
|
||||
<Link href={`/${fund.slug}/register?nextAction=membership`}>
|
||||
<Button variant="light" size="lg">
|
||||
Get Annual Membership
|
||||
</Button>
|
||||
@@ -118,35 +111,6 @@ const Home: NextPage<{ projects: any }> = ({ projects }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{session.status !== 'authenticated' && (
|
||||
<>
|
||||
<Dialog open={loginIsOpen} onOpenChange={setLoginIsOpen}>
|
||||
<DialogContent>
|
||||
<LoginFormModal
|
||||
close={() => setLoginIsOpen(false)}
|
||||
openRegisterModal={() => setRegisterIsOpen(true)}
|
||||
openPasswordResetModal={() => setPasswordResetIsOpen(true)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={registerIsOpen} onOpenChange={setRegisterIsOpen}>
|
||||
<DialogContent>
|
||||
<RegisterFormModal
|
||||
openLoginModal={() => setLoginIsOpen(true)}
|
||||
close={() => setRegisterIsOpen(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={passwordResetIsOpen} onOpenChange={setPasswordResetIsOpen}>
|
||||
<DialogContent>
|
||||
<PasswordResetFormModal close={() => setPasswordResetIsOpen(false)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,22 +7,13 @@ import { useSession } from 'next-auth/react'
|
||||
import { getProjects } from '../../utils/md'
|
||||
import CustomLink from '../../components/CustomLink'
|
||||
import { Button } from '../../components/ui/button'
|
||||
import { Dialog, DialogContent } from '../../components/ui/dialog'
|
||||
import ProjectList from '../../components/ProjectList'
|
||||
import LoginFormModal from '../../components/LoginFormModal'
|
||||
import RegisterFormModal from '../../components/RegisterFormModal'
|
||||
import PasswordResetFormModal from '../../components/PasswordResetFormModal'
|
||||
import { trpc } from '../../utils/trpc'
|
||||
import { funds } from '../../utils/funds'
|
||||
|
||||
const fund = funds['privacyguides']
|
||||
|
||||
const Home: NextPage<{ projects: any }> = ({ projects }) => {
|
||||
const [donateModalOpen, setDonateModalOpen] = useState(false)
|
||||
const [memberModalOpen, setMemberModalOpen] = useState(false)
|
||||
const [registerIsOpen, setRegisterIsOpen] = useState(false)
|
||||
const [loginIsOpen, setLoginIsOpen] = useState(false)
|
||||
const [passwordResetIsOpen, setPasswordResetIsOpen] = useState(false)
|
||||
const session = useSession()
|
||||
|
||||
const userHasMembershipQuery = trpc.donation.userHasMembership.useQuery(
|
||||
@@ -64,12 +55,14 @@ const Home: NextPage<{ projects: any }> = ({ projects }) => {
|
||||
|
||||
{!userHasMembershipQuery.data && (
|
||||
<>
|
||||
{session.status !== 'authenticated' ? (
|
||||
<Button onClick={() => setRegisterIsOpen(true)} variant="light" size="lg">
|
||||
Get Annual Membership
|
||||
</Button>
|
||||
{session.status === 'authenticated' ? (
|
||||
<Link href={`/${fund.slug}/membership`}>
|
||||
<Button variant="light" size="lg">
|
||||
Get Annual Membership
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<Link href={`/${fund.slug}/membership/${fund.slug}`}>
|
||||
<Link href={`/${fund.slug}/register?nextAction=membership`}>
|
||||
<Button variant="light" size="lg">
|
||||
Get Annual Membership
|
||||
</Button>
|
||||
@@ -89,14 +82,15 @@ const Home: NextPage<{ projects: any }> = ({ projects }) => {
|
||||
|
||||
<div className="flex flex-row flex-wrap">
|
||||
<p className="text-md leading-7 text-gray-500">
|
||||
Donate to
|
||||
<CustomLink href={`https://www.privacyguides.org/en/`}>
|
||||
{' '}
|
||||
Privacy Guides
|
||||
</CustomLink> and support our mission to defend digital rights and
|
||||
spread the word about mass surveillance programs and other daily privacy invasions.
|
||||
You can help Privacy Guides researchers, activists, and maintainers create informative content,
|
||||
host private digital services, and protect privacy rights at a time when the world needs it most.
|
||||
Donate to
|
||||
<CustomLink href={`https://www.privacyguides.org/en/`}>
|
||||
{' '}
|
||||
Privacy Guides
|
||||
</CustomLink>{' '}
|
||||
and support our mission to defend digital rights and spread the word about mass
|
||||
surveillance programs and other daily privacy invasions. You can help Privacy Guides
|
||||
researchers, activists, and maintainers create informative content, host private
|
||||
digital services, and protect privacy rights at a time when the world needs it most.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -122,35 +116,6 @@ const Home: NextPage<{ projects: any }> = ({ projects }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{session.status !== 'authenticated' && (
|
||||
<>
|
||||
<Dialog open={loginIsOpen} onOpenChange={setLoginIsOpen}>
|
||||
<DialogContent>
|
||||
<LoginFormModal
|
||||
close={() => setLoginIsOpen(false)}
|
||||
openRegisterModal={() => setRegisterIsOpen(true)}
|
||||
openPasswordResetModal={() => setPasswordResetIsOpen(true)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={registerIsOpen} onOpenChange={setRegisterIsOpen}>
|
||||
<DialogContent>
|
||||
<RegisterFormModal
|
||||
openLoginModal={() => setLoginIsOpen(true)}
|
||||
close={() => setRegisterIsOpen(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={passwordResetIsOpen} onOpenChange={setPasswordResetIsOpen}>
|
||||
<DialogContent>
|
||||
<PasswordResetFormModal close={() => setPasswordResetIsOpen(false)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ export const authRouter = router({
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string().min(8),
|
||||
fundSlug: z.enum(fundSlugs),
|
||||
nextAction: z.enum(['membership']).optional(),
|
||||
_addMailingAddress: z.boolean(),
|
||||
address: z
|
||||
.object({
|
||||
@@ -143,7 +144,7 @@ export const authRouter = router({
|
||||
from: env.SES_VERIFIED_SENDER,
|
||||
to: input.email,
|
||||
subject: 'Verify your email',
|
||||
html: `<a href="${env.APP_URL}/${input.fundSlug}/verify-email/${emailVerifyToken}" target="_blank">Verify email</a>`,
|
||||
html: `<a href="${env.APP_URL}/${input.fundSlug}/verify-email/${emailVerifyToken}${input.nextAction === 'membership' ? `?nextAction=membership` : ''}" target="_blank">Verify email</a>`,
|
||||
})
|
||||
}),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user