Skip to content

Commit

Permalink
Merge pull request #10 from vscubing/feat/extra-reson-prompt
Browse files Browse the repository at this point in the history
Feat/extra reson prompt
  • Loading branch information
bohdancho authored Jan 13, 2025
2 parents 9c1eab2 + 0e739d6 commit 81497e0
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 17 deletions.
24 changes: 21 additions & 3 deletions src/components/ui/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ export type InputProps = ComponentProps<'input'> & {
error?: boolean
}

const Input = forwardRef<HTMLInputElement, InputProps>(({ className, type = 'text', error = false, ...props }, ref) => {
const Input = forwardRef<HTMLInputElement, InputProps>(({ className, error = false, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'text-large h-11 rounded-lg bg-black-100 px-4 placeholder-grey-40 ring-grey-40 transition-shadow duration-200 ease-in-out focus-visible:outline-none focus-visible:ring-1',
{ 'ring-1 ring-red-80': error },
Expand All @@ -21,4 +20,23 @@ const Input = forwardRef<HTMLInputElement, InputProps>(({ className, type = 'tex
})
Input.displayName = 'Input'

export { Input }
export type TextAreaProps = ComponentProps<'textarea'> & {
error?: boolean
}

const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(({ className, error = false, ...props }, ref) => {
return (
<textarea
className={cn(
'text-large rounded-lg bg-black-100 px-4 py-[0.625rem] placeholder-grey-40 ring-grey-40 transition-shadow duration-200 ease-in-out focus-visible:outline-none focus-visible:ring-1',
{ 'ring-1 ring-red-80': error },
className,
)}
ref={ref}
{...props}
/>
)
})
TextArea.displayName = 'TextArea'

export { Input, TextArea }
3 changes: 3 additions & 0 deletions src/components/ui/popovers/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const DialogTrigger = DialogPrimitive.Trigger

const DialogPortal = DialogPrimitive.Portal

const DialogDescription = DialogPrimitive.Description

const DialogOverlay = forwardRef<
ElementRef<typeof DialogPrimitive.Overlay>,
ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> & { withCubes?: boolean }
Expand Down Expand Up @@ -82,4 +84,5 @@ export {
DialogContent,
DialogFooter,
DialogTitle,
DialogDescription,
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
import { PrimaryButton, SecondaryButton } from '@/components/ui'
import {
PrimaryButton,
SecondaryButton,
Dialog,
DialogPortal,
DialogOverlay,
DialogCloseCross,
DialogClose,
DialogTrigger,
DialogContent,
DialogFooter,
DialogTitle,
TextArea,
DialogDescription,
} from '@/components/ui'
import { SolveContestStateDTO } from '../types'
import { SolvePanel } from './SolvePanel'
import { ReactNode, useState } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { BaseDialogButton } from '@/components/ui/popovers/BaseDialog'

export function CurrentSolve({
areActionsDisabled,
Expand All @@ -13,7 +32,7 @@ export function CurrentSolve({
areActionsDisabled: boolean
number: number
currentSolve: SolveContestStateDTO['currentSolve']
onChangeToExtra: () => void
onChangeToExtra: (reason: string) => void
onSolveInit: () => void
onSolveSubmit: () => void
}) {
Expand All @@ -33,14 +52,14 @@ export function CurrentSolve({
) : (
<div className='flex gap-1'>
{currentSolve.canChangeToExtra && (
<SecondaryButton
size='sm'
className='w-[5.25rem]'
onClick={onChangeToExtra}
disabled={areActionsDisabled}
>
Extra
</SecondaryButton>
<ExtraReasonPrompt
onChangeToExtra={onChangeToExtra}
trigger={
<SecondaryButton size='sm' className='w-[5.25rem]' disabled={areActionsDisabled}>
Extra
</SecondaryButton>
}
/>
)}
<PrimaryButton size='sm' className='w-[5.25rem]' onClick={onSolveSubmit} disabled={areActionsDisabled}>
Submit
Expand All @@ -51,3 +70,71 @@ export function CurrentSolve({
/>
)
}

const REASON_MIN_LENGTH = 3
const reasonFormSchema = z.object({
reason: z.string().trim().min(REASON_MIN_LENGTH, 'The reason should be at least 3 characters long'),
})
type ReasonForm = z.infer<typeof reasonFormSchema>

function ExtraReasonPrompt({
trigger,
onChangeToExtra,
}: {
trigger: ReactNode
onChangeToExtra: (reason: string) => void
}) {
const {
register,
handleSubmit,
reset: resetForm,
formState: { errors },
} = useForm<ReasonForm>({ resolver: zodResolver(reasonFormSchema) })

const [open, setOpen] = useState(false)

function onSubmit({ reason }: ReasonForm) {
onChangeToExtra(reason)
setOpen(false)
}

return (
<Dialog
onOpenChange={(newOpen) => {
resetForm()
setOpen(newOpen)
}}
open={open}
>
<DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogPortal>
<DialogOverlay withCubes={false} className='bg-black-1000/25' />
<DialogContent className='max-w-[35rem] p-0'>
<form className='relative h-full w-full px-24 py-16' onSubmit={handleSubmit(onSubmit)}>
<DialogCloseCross className='absolute right-4 top-4' />
<DialogTitle className='mb-4'>Need an Extra attempt?</DialogTitle>
<DialogDescription className='mb-8 text-center text-[0.875rem] leading-[1.5] text-grey-20'>
To request an extra attempt, please tell us what went wrong. This helps ensure extras are used
thoughtfully
</DialogDescription>
<label className='mb-8 block'>
<TextArea
error={!!errors.reason}
{...register('reason')}
className='mb-1 h-24 w-full'
placeholder='Type your reason here'
/>
<span className='caption'>{errors.reason?.message}</span>
</label>
<DialogFooter>
<DialogClose version='secondary'>Cancel</DialogClose>
<BaseDialogButton type='submit' version='primary' disabled={!!errors.reason}>
Submit
</BaseDialogButton>
</DialogFooter>
</form>
</DialogContent>
</DialogPortal>
</Dialog>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ export function SolveContestForm({ state: { currentSolve, submittedSolveSet }, i
initSolve(currentSolve.scramble.moves, (result) => void onSolveFinish(result))
}

async function handleSolveAction(action: 'change_to_extra' | 'submit') {
await solveAction(action)
async function handleSolveAction(payload: { type: 'change_to_extra'; reason: string } | { type: 'submit' }) {
// TODO: also pass the reason for extras once backend is ready
await solveAction(payload.type)
}

const currentSolveNumber = (submittedSolveSet?.length ?? 0) + 1
Expand Down Expand Up @@ -61,9 +62,9 @@ export function SolveContestForm({ state: { currentSolve, submittedSolveSet }, i
<CurrentSolve
areActionsDisabled={isPending}
currentSolve={currentSolve}
onChangeToExtra={() => handleSolveAction('change_to_extra')}
onChangeToExtra={(reason) => handleSolveAction({ type: 'change_to_extra', reason })}
onSolveInit={handleInitSolve}
onSolveSubmit={() => handleSolveAction('submit')}
onSolveSubmit={() => handleSolveAction({ type: 'submit' })}
number={currentSolveNumber}
/>
</div>
Expand Down

0 comments on commit 81497e0

Please sign in to comment.