Skip to content

Commit

Permalink
added base work for waitlist integration (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
arkhurst authored Mar 13, 2024
1 parent 835893f commit 797c160
Show file tree
Hide file tree
Showing 8 changed files with 1,214 additions and 1,134 deletions.
50 changes: 50 additions & 0 deletions apps/wait-list/app/api/waitlist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {fetchClient} from '@/lib/transport/index.ts'
import {useMutation} from '@tanstack/react-query'

interface JoinWaitListRequest {
userType: User
name: string
email?: string
phoneNumber: string
}

interface JoinWaitListResponse {
id: string
name: string
phoneNumber: string
email: string
type: User
}

export const joinWaitListFetcher = async (request: JoinWaitListRequest) => {
try {
const response = await fetchClient<ApiResponse<JoinWaitListResponse>>(
'/v1/waitlists',
{
method: 'POST',
body: JSON.stringify(request),
},
)

if (!response.parsedBody.status && response.parsedBody.errorMessage) {
throw new Error(response.parsedBody.errorMessage)
}

return response.parsedBody.data
} catch (error: unknown) {
if (error instanceof Error) {
throw error
}

// Error from server.
if (error instanceof Response) {
const response = await error.json()
throw new Error(response.errorMessage)
}
}
}

export const useJoinWaitList = () =>
useMutation({
mutationFn: joinWaitListFetcher,
})
8 changes: 5 additions & 3 deletions apps/wait-list/app/components/spring-modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ type Props = {
setIsOpen: Dispatch<SetStateAction<boolean>>
children: ReactNode
className?: string
canClose?: boolean
}

export const SpringModal = ({
isOpen,
setIsOpen,
children,
className = 'bg-white text-gray-800 p-6 rounded-lg w-full max-w-xl shadow-xl cursor-default relative overflow-hidden',
canClose = true,
className = 'relative w-full max-w-xl p-6 overflow-hidden text-gray-800 bg-white rounded-lg shadow-xl cursor-default',
}: Props) => {
return (
<AnimatePresence>
Expand All @@ -21,8 +23,8 @@ export const SpringModal = ({
initial={{opacity: 0}}
animate={{opacity: 1}}
exit={{opacity: 0}}
onClick={() => setIsOpen(false)}
className="bg-slate-900/20 backdrop-blur p-8 fixed inset-0 z-50 grid place-items-center overflow-y-scroll cursor-pointer"
onClick={canClose ? () => setIsOpen(false) : () => null}
className="fixed inset-0 z-50 grid p-8 overflow-y-scroll cursor-pointer bg-slate-900/20 backdrop-blur place-items-center"
>
<motion.div
initial={{scale: 0, rotate: '12.5deg'}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ const accountTypeList: AccountTypeOption[] = [
{
id: 1,
title: 'Photographer/Creator',
description: 'For photographers or content creator.',
description: 'Freelancers who want showcase your creativity.',
value: 'CREATOR',
},
{
id: 2,
title: 'Client',
description: 'For users who are a client seeking services.',
description: 'Users who want to access your event memories.',
value: 'CLIENT',
},
]
Expand All @@ -38,7 +38,7 @@ export default function AccountType({
return (
<RadioGroup value={selectedAccountType} onChange={setSelectedAccountType}>
<RadioGroup.Label className="block text-sm font-medium leading-6 text-gray-900 mt-3">
Select your user type
How do you want to use mfoni?
</RadioGroup.Label>

<div className="mt-2 grid grid-cols-1 gap-y-4 lg:gap-y-6 sm:grid-cols-2 sm:gap-x-4">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {useCallback, useEffect, useState} from 'react'
import AccountType, {
type AccountTypeOption,
} from '../../components/account-type.tsx'
import {useJoinWaitList} from '@/api/waitlist.ts'
import {toast} from 'react-hot-toast'
import {Loader} from '@/components/loader/index.tsx'

type Props = {
show: boolean
Expand All @@ -17,57 +20,89 @@ type Props = {
}

interface FormValues {
role: User
userType: User
name: string
email?: string
phoneNumber: string
}

const schema = Yup.object().shape({
role: Yup.string()
userType: Yup.string()
.oneOf(['CLIENT', 'CREATOR'])
.default('CLIENT')
.required('Role is required'),
name: Yup.string().required('Full name is required'),
phoneNumber: Yup.string().required('Phone number is required'),
email: Yup.string(),
email: Yup.string().email('Invalid email address'),
})

export default function WaitListForm({show, setShow, handleClose}: Props) {
const [selectedAccountType, setSelectedAccountType] = useState<
AccountTypeOption | undefined
>()

const {
register,
handleSubmit,
formState: {errors},
setValue,
reset,
} = useForm<FormValues>({
resolver: yupResolver(schema),
})

const {mutate, isPending} = useJoinWaitList()

const hasSelectedAccountType = !!selectedAccountType

useEffect(() => {
if (selectedAccountType) {
setValue('role', selectedAccountType.value)
setValue('userType', selectedAccountType.value)
}
}, [selectedAccountType, setValue])

const onSubmit = useCallback((data: FormValues) => {
console.log(data)
}, [])
const onSubmit = useCallback(
(formData: FormValues) => {
if (!hasSelectedAccountType) {
return toast.error('Please select an account type')
}
mutate(formData, {
onSuccess: async () => {
toast.success('You have been added to the waitlist!', {
id: 'waitlist-success',
})
reset({
name: '',
email: '',
userType: undefined,
phoneNumber: '',
})
setSelectedAccountType(undefined)
setShow(false)
},
onError: error => {
toast.error(error.message, {id: 'waitlist-error'})
},
})
},
[hasSelectedAccountType, setSelectedAccountType, setShow, reset, mutate],
)

return (
<SpringModal isOpen={show} setIsOpen={setShow}>
<SpringModal canClose={!isPending} isOpen={show} setIsOpen={setShow}>
<form onSubmit={handleSubmit(onSubmit)}>
<h3 className="lg:text-xl text-lg font-medium leading-6 text-center text-gray-900">
<h3 className="text-lg font-medium leading-6 text-center text-gray-900 lg:text-xl">
Join Our Waitlist
</h3>
<div className="mt-2">
<p className="lg:text-base text-sm text-center text-gray-500 max-w-2xl mx-auto">
<p className="max-w-2xl mx-auto mb-2 text-sm text-center text-gray-500 lg:text-base">
Sign up now to join our waitlist and be among the first to access
exclusive features and updates!
</p>

<AccountType
selectedAccountType={selectedAccountType}
setSelectedAccountType={setSelectedAccountType}
/>
<div className="mt-3">
<div className="flex justify-between">
<label
Expand All @@ -82,23 +117,22 @@ export default function WaitListForm({show, setShow, handleClose}: Props) {
type="text"
{...register('name')}
className={classNames(
'block w-full rounded-md border-0 py-2.5 lg:py-2 text-gray-900 shadow-nome ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 text-xs lg:text-sm sm:leading-6 ',
errors.name ? 'ring-red-500' : '',
'block w-full rounded-md border-0 py-2.5 lg:py-2 text-gray-900 shadow-nome ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset text-xs lg:text-sm sm:leading-6 ',
errors.name ? 'ring-red-500' : 'focus:ring-blue-600 ',
)}
placeholder="Enter your full name"
aria-describedby="name"
/>
{errors.name ? (
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<ExclamationCircleIcon
className="h-5 w-5 text-red-500"
className="w-5 h-5 text-red-500"
aria-hidden="true"
/>
</div>
) : null}
</div>
</div>

<div className="mt-3">
<div className="flex justify-between">
<label
Expand All @@ -113,23 +147,22 @@ export default function WaitListForm({show, setShow, handleClose}: Props) {
type="text"
{...register('phoneNumber')}
className={classNames(
'block w-full rounded-md border-0 py-2.5 lg:py-2 text-gray-900 shadow-nome ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 text-xs lg:text-sm sm:leading-6 ',
errors.phoneNumber ? 'ring-red-500' : '',
'block w-full rounded-md border-0 py-2.5 lg:py-2 text-gray-900 shadow-nome ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset text-xs lg:text-sm sm:leading-6 ',
errors.phoneNumber ? 'ring-red-500' : 'focus:ring-blue-600',
)}
placeholder="Enter your phone number"
aria-describedby="phoneNumber"
/>
{errors.phoneNumber ? (
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<ExclamationCircleIcon
className="h-5 w-5 text-red-500"
className="w-5 h-5 text-red-500"
aria-hidden="true"
/>
</div>
) : null}
</div>
</div>

<div className="mt-3">
<div className="flex flex-row items-center justify-between">
<label
Expand All @@ -147,28 +180,23 @@ export default function WaitListForm({show, setShow, handleClose}: Props) {
type="text"
{...register('email')}
className={classNames(
'block w-full rounded-md border-0 py-2.5 lg:py-2 text-gray-900 shadow-nome ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 text-xs lg:text-sm sm:leading-6 ',
errors.email ? 'ring-red-500' : '',
'block w-full rounded-md border-0 py-2.5 lg:py-2 text-gray-900 shadow-nome ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset text-xs lg:text-sm sm:leading-6 ',
errors.email ? 'ring-red-500' : 'focus:ring-blue-600',
)}
placeholder="Enter your email address"
aria-describedby="email-optional"
/>
{errors.email ? (
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<ExclamationCircleIcon
className="h-5 w-5 text-red-500"
className="w-5 h-5 text-red-500"
aria-hidden="true"
/>
</div>
) : null}
</div>
</div>
<AccountType
selectedAccountType={selectedAccountType}
setSelectedAccountType={setSelectedAccountType}
/>
</div>

<div className="mt-8">
<Button
variant="unstyled"
Expand All @@ -186,6 +214,11 @@ export default function WaitListForm({show, setShow, handleClose}: Props) {
Close
</Button>
</div>
{isPending ? (
<div className="absolute inset-0 flex items-center justify-center w-full h-full bg-black/40">
<Loader color="fill-white" />
</div>
) : null}
</form>
</SpringModal>
)
Expand Down
3 changes: 3 additions & 0 deletions apps/wait-list/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {NODE_ENV} from './constants/index.ts'
import tailwindStyles from '@/styles/tailwind.css'
import {Providers} from './providers/index.tsx'
import {RouteLoader} from './components/loader/route-loader.tsx'
import {Toaster} from 'react-hot-toast'

export const links: LinksFunction = () => {
return [
Expand Down Expand Up @@ -76,6 +77,8 @@ function Document({children}: PropsWithChildren) {
</head>
<body>
{children}
<Toaster position="bottom-center" />

<ScrollRestoration />
<script
src="https://accounts.google.com/gsi/client"
Expand Down
1 change: 1 addition & 0 deletions apps/wait-list/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "7.49.3",
"react-hot-toast": "2.4.1",
"react-intersection-observer": "9.5.3",
"tailwind-merge": "^2.2.1",
"yup": "1.3.3"
Expand Down
5 changes: 5 additions & 0 deletions apps/wait-list/types/misc.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
interface ApiResponse<T> {
data: T
errorMessage: Nullable<string>
status: boolean
}
Loading

0 comments on commit 797c160

Please sign in to comment.