diff --git a/components/Header.tsx b/components/Header.tsx index acc9f62..8989f91 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -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' && ( <> - - - - - - - - - setLoginIsOpen(false)} - openRegisterModal={() => setRegisterIsOpen(true)} - openPasswordResetModal={() => setPasswordResetIsOpen(true)} - /> - - + + - - - - - - - - - setRegisterIsOpen(false)} - openLoginModal={() => setLoginIsOpen(true)} - /> - - + + + + + + + + )} @@ -164,12 +126,6 @@ const Header = () => { {!!fundSlug && } - - - - setPasswordResetIsOpen(false)} /> - - ) } diff --git a/components/Layout.tsx b/components/Layout.tsx index fa82918..4baece9 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -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) => {
-
{children}
+
{children}
diff --git a/components/PerkCard.tsx b/components/PerkCard.tsx index c8bf68c..73f2457 100644 --- a/components/PerkCard.tsx +++ b/components/PerkCard.tsx @@ -17,7 +17,7 @@ const PerkCard: React.FC = ({ perk }) => {
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 - -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({ - 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(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 ( -
-
- - - {perk.images.map((image) => ( - - {perk.name} - - ))} - - - - -
- -
- -
-
- - - {perk.images.map((image) => ( - - {perk.name} - - ))} - - - - -
- -
-
-

{perk.name}

- {!costEstimate &&

{perk.description}

} - {!costEstimate && perk.productDetailsUrl && ( - - View product details - - )} - {!!costEstimate && printfulSyncVariantId && ( -

- { - getPrintfulProductVariantsQuery.data?.find( - (variant) => variant.id === Number(printfulSyncVariantId) - )?.name - } -

- )} -
- - {!costEstimate && ( -
- - -

- {pointFormat.format(perk.price)}{' '} - points -

- - - You have {pointFormat.format(balance)} points - -
- )} -
-
- -
- {perk.needsShippingAddress && hasEnoughBalance && !costEstimate && ( - <> - {perk.needsShippingAddress && !!getPrintfulProductVariantsQuery.data && ( -
- ( - - Options * - - - - )} - /> -
- )} - - {perk.needsShippingAddress && !getPrintfulProductVariantsQuery.data && } - - {getUserAttributesQuery.data?.addressLine1 && ( - ( - - - - -
- Use saved mailing address -
-
- )} - /> - )} - - ( - - Address line 1 * - - - - - - )} - /> - - ( - - Address line 2 - - - - - - )} - /> - - ( - - Country * - setCountrySelectOpen(open)} - > - -
- - - -
-
- - - - - No country found. - - {shippingCountryOptions.map((country) => ( - ( - form.setValue('shippingCountry', country.value, { - shouldValidate: true, - }), - setCountrySelectOpen(false) - )} - > - {country.label} - - - ))} - - - - -
- -
- )} - /> - - {!!shippingStateOptions.length && ( - ( - - State * - setStateSelectOpen(open)} - > - -
- - - -
-
- - - - - No state found. - - {shippingStateOptions.map((state) => ( - ( - form.setValue('shippingState', state.value, { - shouldValidate: true, - }), - setStateSelectOpen(false) - )} - > - {state.label} - - - ))} - - - - -
- -
- )} - /> - )} - - ( - - City * - - - - - - )} - /> - - ( - - Postal code * - - - - - - )} - /> - - ( - - Phone number * - - - - - - )} - /> - - {shippingCountry === 'BR' && ( - ( - - Tax number (CPF) * - - - - - - )} - /> - )} - - - Price subject to change depending on your region. - - - - - )} - - {!!costEstimate && ( -
- - - - Item - {pointFormat.format(costEstimate.product)} points - - - - Shipping - {pointFormat.format(costEstimate.shipping)} points - - - Tax - {pointFormat.format(costEstimate.tax)} points - - - Total - - - {pointFormat.format(costEstimate.total)} - {' '} - points - - - -
- - - You have {pointFormat.format(balance)} points - -
- )} - - {((perk.needsShippingAddress && !!costEstimate) || !perk.needsShippingAddress) && ( - - )} -
-
- -
- ) -} - -export default PerkPurchaseFormModal diff --git a/components/ProjectCard.tsx b/components/ProjectCard.tsx index 6be249d..1bc330b 100644 --- a/components/ProjectCard.tsx +++ b/components/ProjectCard.tsx @@ -33,7 +33,7 @@ const ProjectCard: React.FC = ({ project, customImageStyles })
, - React.ComponentPropsWithoutRef & - VariantProps + React.ComponentPropsWithoutRef & VariantProps >(({ className, variant, ...props }, ref) => { return ( Donate to {project.title} -
+

Donate to {project.title}

@@ -256,7 +250,9 @@ function DonationPage({ fund: fundSlug, slug, project }: Props) { name="taxDeductible" render={({ field }) => ( - Do you want this donation to potentially qualify for a tax deduction? (US only) + + Do you want this donation to potentially qualify for a tax deduction? (US only) +

Want to support more projects and receive optional perks?

- + diff --git a/components/PasswordResetFormModal.tsx b/pages/[fund]/forgot-password.tsx similarity index 62% rename from components/PasswordResetFormModal.tsx rename to pages/[fund]/forgot-password.tsx index 4df690c..9df0360 100644 --- a/components/PasswordResetFormModal.tsx +++ b/pages/[fund]/forgot-password.tsx @@ -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 -type Props = { close: () => void } - -function PasswordResetFormModal({ close }: Props) { +function ForgotPassword() { const { toast } = useToast() const turnstileRef = useRef() @@ -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 ( - <> - - Reset Password - Recover your account. - +
+

Request password reset

@@ -80,8 +83,16 @@ function PasswordResetFormModal({ close }: Props) {
- +
) } -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!}` } } + } +} diff --git a/components/LoginFormModal.tsx b/pages/[fund]/login.tsx similarity index 69% rename from components/LoginFormModal.tsx rename to pages/[fund]/login.tsx index 568aec6..f2dea90 100644 --- a/components/LoginFormModal.tsx +++ b/pages/[fund]/login.tsx @@ -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 -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 ( - <> - - Login - Log into your account. - +
+

Login

@@ -122,7 +123,7 @@ function LoginFormModal({ close, openPasswordResetModal, openRegisterModal }: Pr @@ -158,8 +159,16 @@ function LoginFormModal({ close, openPasswordResetModal, openRegisterModal }: Pr
- +
) } -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!}` } } + } +} diff --git a/pages/[fund]/membership/[slug].tsx b/pages/[fund]/membership.tsx similarity index 90% rename from pages/[fund]/membership/[slug].tsx rename to pages/[fund]/membership.tsx index 492735f..d889e0e 100644 --- a/pages/[fund]/membership/[slug].tsx +++ b/pages/[fund]/membership.tsx @@ -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 ( <> Membership to {project.title} -
+

@@ -273,18 +277,18 @@ function MembershipPage({ fund: fundSlug, project }: Props) { defaultValue={field.value} className="flex flex-row space-x-4 text-gray-700" > - - - - - Yes - No + + + + + Yes + @@ -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) { - 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!] } } } diff --git a/pages/[fund]/perks/[id].tsx b/pages/[fund]/perks/[id].tsx index 56ec9be..8e56201 100644 --- a/pages/[fund]/perks/[id].tsx +++ b/pages/[fund]/perks/[id].tsx @@ -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) {
diff --git a/pages/[fund]/projects/[slug].tsx b/pages/[fund]/projects/[slug].tsx index 2c4c08e..89aaf21 100644 --- a/pages/[fund]/projects/[slug].tsx +++ b/pages/[fund]/projects/[slug].tsx @@ -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 = ({ project, donationStats }) = className="w-full max-w-[700px] mx-auto object-contain xl:hidden" /> -
+
{!project.isFunded && (
@@ -181,7 +173,7 @@ const Project: NextPage = ({ project, donationStats }) =
-
+

Leaderboard

{leaderboardQuery.data?.length ? ( @@ -216,40 +208,11 @@ const Project: NextPage = ({ project, donationStats }) =
- - {session.status !== 'authenticated' && ( - <> - - - setLoginIsOpen(false)} - openRegisterModal={() => setRegisterIsOpen(true)} - openPasswordResetModal={() => setPasswordResetIsOpen(true)} - /> - - - - - - setLoginIsOpen(true)} - close={() => setRegisterIsOpen(false)} - /> - - - - - - setPasswordResetIsOpen(false)} /> - - - - )} ) } diff --git a/components/RegisterFormModal.tsx b/pages/[fund]/register.tsx similarity index 91% rename from components/RegisterFormModal.tsx rename to pages/[fund]/register.tsx index 0fca75a..b787f00 100644 --- a/components/RegisterFormModal.tsx +++ b/pages/[fund]/register.tsx @@ -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 -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 ( - <> - - Register - Start supporting projects today! - +
+

Register

@@ -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 @@ -572,9 +553,16 @@ function RegisterFormModal({ close, openLoginModal }: Props) {
- +
) } -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!}` } } + } +} diff --git a/pages/[fund]/reset-password/[token].tsx b/pages/[fund]/reset-password/[token].tsx index c3703b4..3e3a9f9 100644 --- a/pages/[fund]/reset-password/[token].tsx +++ b/pages/[fund]/reset-password/[token].tsx @@ -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' }) } } diff --git a/pages/[fund]/verify-email/[token].tsx b/pages/[fund]/verify-email/[token].tsx index e04a66c..9077926 100644 --- a/pages/[fund]/verify-email/[token].tsx +++ b/pages/[fund]/verify-email/[token].tsx @@ -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}`) } })() diff --git a/pages/firo/index.tsx b/pages/firo/index.tsx index 8f5404e..57a2f2f 100644 --- a/pages/firo/index.tsx +++ b/pages/firo/index.tsx @@ -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' ? ( - + {session.status === 'authenticated' ? ( + + + ) : ( - + @@ -108,35 +101,6 @@ const Home: NextPage<{ projects: any }> = ({ projects }) => {
- - {session.status !== 'authenticated' && ( - <> - - - setLoginIsOpen(false)} - openRegisterModal={() => setRegisterIsOpen(true)} - openPasswordResetModal={() => setPasswordResetIsOpen(true)} - /> - - - - - - setLoginIsOpen(true)} - close={() => setRegisterIsOpen(false)} - /> - - - - - - setPasswordResetIsOpen(false)} /> - - - - )} ) } diff --git a/pages/general/index.tsx b/pages/general/index.tsx index a7efca5..99e18d9 100644 --- a/pages/general/index.tsx +++ b/pages/general/index.tsx @@ -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' ? ( - + {session.status === 'authenticated' ? ( + + + ) : ( - + @@ -109,35 +102,6 @@ const Home: NextPage<{ projects: any }> = ({ projects }) => {

- - {session.status !== 'authenticated' && ( - <> - - - setLoginIsOpen(false)} - openRegisterModal={() => setRegisterIsOpen(true)} - openPasswordResetModal={() => setPasswordResetIsOpen(true)} - /> - - - - - - setLoginIsOpen(true)} - close={() => setRegisterIsOpen(false)} - /> - - - - - - setPasswordResetIsOpen(false)} /> - - - - )} ) } diff --git a/pages/monero/index.tsx b/pages/monero/index.tsx index 0e197e8..bd02cfa 100644 --- a/pages/monero/index.tsx +++ b/pages/monero/index.tsx @@ -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' ? ( - + {session.status === 'authenticated' ? ( + + + ) : ( - + @@ -118,35 +111,6 @@ const Home: NextPage<{ projects: any }> = ({ projects }) => {
- - {session.status !== 'authenticated' && ( - <> - - - setLoginIsOpen(false)} - openRegisterModal={() => setRegisterIsOpen(true)} - openPasswordResetModal={() => setPasswordResetIsOpen(true)} - /> - - - - - - setLoginIsOpen(true)} - close={() => setRegisterIsOpen(false)} - /> - - - - - - setPasswordResetIsOpen(false)} /> - - - - )} ) } diff --git a/pages/privacyguides/index.tsx b/pages/privacyguides/index.tsx index f5a31a3..d1d5217 100644 --- a/pages/privacyguides/index.tsx +++ b/pages/privacyguides/index.tsx @@ -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' ? ( - + {session.status === 'authenticated' ? ( + + + ) : ( - + @@ -89,14 +82,15 @@ const Home: NextPage<{ projects: any }> = ({ projects }) => {

- Donate to - - {' '} - Privacy Guides - 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 + + {' '} + Privacy Guides + {' '} + 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.

@@ -122,35 +116,6 @@ const Home: NextPage<{ projects: any }> = ({ projects }) => {
- - {session.status !== 'authenticated' && ( - <> - - - setLoginIsOpen(false)} - openRegisterModal={() => setRegisterIsOpen(true)} - openPasswordResetModal={() => setPasswordResetIsOpen(true)} - /> - - - - - - setLoginIsOpen(true)} - close={() => setRegisterIsOpen(false)} - /> - - - - - - setPasswordResetIsOpen(false)} /> - - - - )} ) } diff --git a/server/routers/auth.ts b/server/routers/auth.ts index fbe868f..35e730e 100644 --- a/server/routers/auth.ts +++ b/server/routers/auth.ts @@ -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: `Verify email`, + html: `Verify email`, }) }),