This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: report card modal

+231
+231
src/webapp/features/cards/components/reportCardModal/ReportCardModal.tsx
··· 1 + import { useState } from 'react'; 2 + import { 3 + Button, 4 + Stack, 5 + Modal, 6 + Select, 7 + Textarea, 8 + Text, 9 + Group, 10 + ThemeIcon, 11 + } from '@mantine/core'; 12 + import { BsExclamation } from 'react-icons/bs'; 13 + import { DANGER_OVERLAY_PROPS } from '@/styles/overlays'; 14 + import { 15 + MAX_REPORT_DETAILS_LENGTH, 16 + REPORT_REASONS, 17 + ReportReasonKey, 18 + } from './reportReasons'; 19 + import { IoMdCheckmark } from 'react-icons/io'; 20 + 21 + export type ReportCardModalState = 22 + | { kind: 'form' } 23 + | { kind: 'submitting' } 24 + | { kind: 'success' } 25 + | { kind: 'error'; message: string } 26 + | { kind: 'already-reported' }; 27 + 28 + export interface ReportSubmission { 29 + cardId: string; 30 + reason: ReportReasonKey; 31 + details: string; 32 + } 33 + 34 + interface Props { 35 + isOpen: boolean; 36 + onClose: () => void; 37 + cardId: string; 38 + //if omitted, a mock submission resolves after ~800ms (used by Storybook) 39 + onSubmit?: (input: ReportSubmission) => Promise<void>; 40 + //Forces the modal into a particular state (used by Storybook) 41 + initialState?: ReportCardModalState; 42 + } 43 + 44 + const mockSubmit = (): Promise<void> => 45 + new Promise((resolve) => setTimeout(resolve, 800)); 46 + 47 + export default function ReportCardModal(props: Props) { 48 + const [state, setState] = useState<ReportCardModalState>( 49 + props.initialState ?? { kind: 'form' }, 50 + ); 51 + const [reason, setReason] = useState<ReportReasonKey | null>(null); 52 + const [details, setDetails] = useState(''); 53 + 54 + const resetForm = () => { 55 + setState(props.initialState ?? { kind: 'form' }); 56 + setReason(null); 57 + setDetails(''); 58 + }; 59 + 60 + const detailsRequired = reason === 'other'; 61 + const detailsValid = !detailsRequired || details.trim().length > 0; 62 + const canSubmit = state.kind === 'form' && reason !== null && detailsValid; 63 + 64 + const handleSubmit = async () => { 65 + if (!canSubmit || reason === null) return; 66 + setState({ kind: 'submitting' }); 67 + try { 68 + const submitter = props.onSubmit ?? mockSubmit; 69 + await submitter({ 70 + cardId: props.cardId, 71 + reason, 72 + details: details.trim(), 73 + }); 74 + setState({ kind: 'success' }); 75 + } catch (err) { 76 + setState({ 77 + kind: 'error', 78 + message: 79 + err instanceof Error 80 + ? err.message 81 + : 'Could not submit your report. Please try again.', 82 + }); 83 + } 84 + }; 85 + 86 + return ( 87 + <Modal 88 + opened={props.isOpen} 89 + onClose={props.onClose} 90 + onExitTransitionEnd={resetForm} 91 + title="Report card" 92 + size="sm" 93 + overlayProps={DANGER_OVERLAY_PROPS} 94 + centered 95 + onClick={(e) => e.stopPropagation()} 96 + > 97 + {state.kind === 'already-reported' ? ( 98 + <AlreadyReportedState onClose={props.onClose} /> 99 + ) : state.kind === 'success' ? ( 100 + <SuccessState onClose={props.onClose} /> 101 + ) : ( 102 + <Stack gap="md"> 103 + <Select 104 + label="Why are you reporting this card?" 105 + placeholder="Select a reason" 106 + variant="filled" 107 + withAsterisk 108 + data={REPORT_REASONS.map((r) => ({ 109 + value: r.key, 110 + label: r.label, 111 + }))} 112 + value={reason} 113 + onChange={(value) => setReason(value as ReportReasonKey | null)} 114 + disabled={state.kind === 'submitting'} 115 + renderOption={({ option }) => { 116 + const r = REPORT_REASONS.find((x) => x.key === option.value); 117 + return ( 118 + <Stack gap={2}> 119 + <Text size="sm">{option.label}</Text> 120 + {r?.description && ( 121 + <Text size="xs" c="dimmed"> 122 + {r.description} 123 + </Text> 124 + )} 125 + </Stack> 126 + ); 127 + }} 128 + /> 129 + 130 + <Stack gap={4}> 131 + <Group justify="space-between"> 132 + <Text size="sm" fw={500}> 133 + Additional details{' '} 134 + {detailsRequired ? ( 135 + <Text span c="red"> 136 + * 137 + </Text> 138 + ) : ( 139 + <Text span c="dimmed" size="sm"> 140 + (optional) 141 + </Text> 142 + )} 143 + </Text> 144 + <Text size="xs" c="dimmed"> 145 + {details.length} / {MAX_REPORT_DETAILS_LENGTH} 146 + </Text> 147 + </Group> 148 + <Textarea 149 + placeholder="Add context that will help our moderators" 150 + variant="filled" 151 + rows={3} 152 + maxLength={MAX_REPORT_DETAILS_LENGTH} 153 + value={details} 154 + onChange={(e) => setDetails(e.currentTarget.value)} 155 + disabled={state.kind === 'submitting'} 156 + error={ 157 + detailsRequired && details.length === 0 158 + ? 'Please add a short description' 159 + : undefined 160 + } 161 + /> 162 + </Stack> 163 + 164 + {state.kind === 'error' && ( 165 + <Text size="xs" fw={600} c="red"> 166 + {state.message} 167 + </Text> 168 + )} 169 + 170 + <Group justify="flex-end" gap="xs"> 171 + <Button 172 + variant="subtle" 173 + color="gray" 174 + onClick={props.onClose} 175 + disabled={state.kind === 'submitting'} 176 + > 177 + Cancel 178 + </Button> 179 + <Button 180 + color="red" 181 + onClick={handleSubmit} 182 + loading={state.kind === 'submitting'} 183 + disabled={!canSubmit} 184 + > 185 + Submit report 186 + </Button> 187 + </Group> 188 + </Stack> 189 + )} 190 + </Modal> 191 + ); 192 + } 193 + 194 + function SuccessState({ onClose }: { onClose: () => void }) { 195 + return ( 196 + <Stack gap="md" align="center"> 197 + <ThemeIcon variant="light" color="green" size={'xl'} radius="xl"> 198 + <IoMdCheckmark size={24} /> 199 + </ThemeIcon> 200 + <Stack gap={4} align="center"> 201 + <Text fw={600}>Report received</Text> 202 + <Text size="sm" c="dimmed" ta="center"> 203 + Our moderators will review it. Thanks for helping keep Semble safe — 204 + your report stays anonymous. 205 + </Text> 206 + </Stack> 207 + <Button onClick={onClose} fullWidth> 208 + Ok 209 + </Button> 210 + </Stack> 211 + ); 212 + } 213 + 214 + function AlreadyReportedState({ onClose }: { onClose: () => void }) { 215 + return ( 216 + <Stack gap="md" align="center"> 217 + <ThemeIcon variant="light" color="gray" size={'xl'} radius="xl"> 218 + <BsExclamation size={24} /> 219 + </ThemeIcon> 220 + <Stack gap={4} align="center"> 221 + <Text fw={600}>You&apos;ve already reported this card</Text> 222 + <Text size="sm" c="dimmed" ta="center"> 223 + Our moderators are reviewing it. 224 + </Text> 225 + </Stack> 226 + <Button onClick={onClose} fullWidth> 227 + Close 228 + </Button> 229 + </Stack> 230 + ); 231 + }