@@ -12,33 +12,28 @@ import {
1212import { X } from '@sim/emcn/icons'
1313import { createLogger } from '@sim/logger'
1414import { useMutation } from '@tanstack/react-query'
15- import imageCompression from 'browser-image-compression'
1615import Image from 'next/image'
1716import { Controller , useForm } from 'react-hook-form'
1817import { z } from 'zod'
18+ import { HELP_REQUEST_TYPES } from '@/lib/api/contracts/common'
19+ import {
20+ ACCEPTED_HELP_IMAGE_TYPES ,
21+ compressHelpImage ,
22+ DEFAULT_HELP_REQUEST_TYPE ,
23+ HELP_REQUEST_TYPE_OPTIONS ,
24+ submitHelpRequest ,
25+ validateHelpImage ,
26+ } from '@/lib/help/request'
1927
2028const logger = createLogger ( 'HelpModal' )
2129
22- const MAX_FILE_SIZE = 20 * 1024 * 1024
23- const TARGET_SIZE_MB = 2
24- const ACCEPTED_IMAGE_TYPES = [ 'image/jpeg' , 'image/jpg' , 'image/png' , 'image/webp' , 'image/gif' ]
25-
2630const SCROLL_DELAY_MS = 100
2731const SUCCESS_RESET_DELAY_MS = 2000
2832
29- const DEFAULT_REQUEST_TYPE = 'bug'
30-
31- const REQUEST_TYPE_OPTIONS = [
32- { label : 'Bug report' , value : 'bug' } ,
33- { label : 'Feedback' , value : 'feedback' } ,
34- { label : 'Feature request' , value : 'feature_request' } ,
35- { label : 'Other' , value : 'other' } ,
36- ]
37-
3833const formSchema = z . object ( {
3934 subject : z . string ( ) . min ( 1 , 'Subject is required' ) ,
4035 message : z . string ( ) . min ( 1 , 'Message is required' ) ,
41- type : z . enum ( [ 'bug' , 'feedback' , 'feature_request' , 'other' ] , {
36+ type : z . enum ( HELP_REQUEST_TYPES , {
4237 error : 'Please select a request type' ,
4338 } ) ,
4439} )
@@ -63,58 +58,6 @@ interface SubmitHelpVariables {
6358 workspaceId ?: string
6459}
6560
66- async function compressImage ( file : File ) : Promise < File > {
67- if ( file . size < TARGET_SIZE_MB * 1024 * 1024 || file . type === 'image/gif' ) {
68- return file
69- }
70-
71- try {
72- const compressedFile = await imageCompression ( file , {
73- maxSizeMB : TARGET_SIZE_MB ,
74- maxWidthOrHeight : 1920 ,
75- useWebWorker : true ,
76- fileType : file . type ,
77- initialQuality : 0.8 ,
78- alwaysKeepResolution : true ,
79- } )
80-
81- return new File ( [ compressedFile ] , file . name , {
82- type : file . type ,
83- lastModified : Date . now ( ) ,
84- } )
85- } catch ( error ) {
86- logger . warn ( 'Image compression failed, using original file:' , { error } )
87- return file
88- }
89- }
90-
91- async function submitHelpRequest ( { data, images, workflowId, workspaceId } : SubmitHelpVariables ) {
92- const formData = new FormData ( )
93- formData . append ( 'subject' , data . subject )
94- formData . append ( 'message' , data . message )
95- formData . append ( 'type' , data . type )
96- if ( workspaceId ) formData . append ( 'workspaceId' , workspaceId )
97- formData . append ( 'userAgent' , navigator . userAgent )
98- if ( workflowId ) {
99- formData . append ( 'workflowId' , workflowId )
100- }
101-
102- images . forEach ( ( image , index ) => {
103- formData . append ( `image_${ index } ` , image )
104- } )
105-
106- // boundary-raw-fetch: multipart/form-data submission with image attachments, requestJson only supports JSON bodies
107- const response = await fetch ( '/api/help' , {
108- method : 'POST' ,
109- body : formData ,
110- } )
111-
112- if ( ! response . ok ) {
113- const errorData = await response . json ( ) . catch ( ( ) => null )
114- throw new Error ( errorData ?. error || 'Failed to submit help request' )
115- }
116- }
117-
11861export function HelpModal ( { open, onOpenChange, workflowId, workspaceId } : HelpModalProps ) {
11962 const scrollContainerRef = useRef < HTMLDivElement > ( null )
12063 const imagesRef = useRef < ImageWithPreview [ ] > ( [ ] )
@@ -128,13 +71,13 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
12871 defaultValues : {
12972 subject : '' ,
13073 message : '' ,
131- type : DEFAULT_REQUEST_TYPE ,
74+ type : DEFAULT_HELP_REQUEST_TYPE ,
13275 } ,
13376 mode : 'onSubmit' ,
13477 } )
13578
13679 const helpMutation = useMutation ( {
137- mutationFn : submitHelpRequest ,
80+ mutationFn : ( variables : SubmitHelpVariables ) => submitHelpRequest ( variables ) ,
13881 onSuccess : ( _data , variables ) => {
13982 setSubmitStatus ( 'success' )
14083 reset ( )
@@ -159,7 +102,7 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
159102 reset ( {
160103 subject : '' ,
161104 message : '' ,
162- type : DEFAULT_REQUEST_TYPE ,
105+ type : DEFAULT_HELP_REQUEST_TYPE ,
163106 } )
164107 } else {
165108 const previewsToRevoke = imagesRef . current
@@ -208,17 +151,12 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
208151 let hasError = false
209152
210153 for ( const file of Array . from ( files ) ) {
211- if ( file . size > MAX_FILE_SIZE ) {
212- hasError = true
213- continue
214- }
215-
216- if ( ! ACCEPTED_IMAGE_TYPES . includes ( file . type ) ) {
154+ if ( validateHelpImage ( file ) ) {
217155 hasError = true
218156 continue
219157 }
220158
221- const compressedFile = await compressImage ( file )
159+ const compressedFile = await compressHelpImage ( file )
222160 const imageWithPreview = Object . assign ( compressedFile , {
223161 preview : URL . createObjectURL ( compressedFile ) ,
224162 } ) as ImageWithPreview
@@ -265,7 +203,7 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
265203 title = 'Request'
266204 value = { field . value }
267205 onChange = { field . onChange }
268- options = { REQUEST_TYPE_OPTIONS }
206+ options = { HELP_REQUEST_TYPE_OPTIONS }
269207 placeholder = 'Select a request type'
270208 error = { fieldState . error ?. message }
271209 />
@@ -305,7 +243,7 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
305243 title = 'Attach images (optional)'
306244 label = 'Drop images here or click to browse'
307245 description = 'PNG, JPEG, WebP, GIF (max 20MB each)'
308- accept = { ACCEPTED_IMAGE_TYPES . join ( ',' ) }
246+ accept = { ACCEPTED_HELP_IMAGE_TYPES . join ( ',' ) }
309247 multiple
310248 onChange = { processFiles }
311249 />
0 commit comments