From 28e1ea54e49bcf1848dc0848b172caabf7c0509e Mon Sep 17 00:00:00 2001 From: snowrugar-beep Date: Sat, 30 May 2026 19:57:12 +0000 Subject: [PATCH] feat: add BidSubmissionForm with auto and manual price entry and React Query mutation Closes #872 --- .../components/BidForm/BidSubmissionForm.tsx | 172 ++++++++++++++++++ frontend/package/components/BidForm/index.ts | 2 + 2 files changed, 174 insertions(+) create mode 100644 frontend/package/components/BidForm/BidSubmissionForm.tsx create mode 100644 frontend/package/components/BidForm/index.ts diff --git a/frontend/package/components/BidForm/BidSubmissionForm.tsx b/frontend/package/components/BidForm/BidSubmissionForm.tsx new file mode 100644 index 00000000..702695e2 --- /dev/null +++ b/frontend/package/components/BidForm/BidSubmissionForm.tsx @@ -0,0 +1,172 @@ +'use client'; + +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { toast } from 'sonner'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { apiClient } from '@/lib/api/client'; + +const CURRENCIES = ['USD', 'EUR', 'GBP', 'NGN', 'KES', 'ZAR']; + +const bidSchema = z.object({ + proposed_price: z.coerce.number().positive('Price must be a positive number'), + currency: z.string().length(3, 'Select a currency'), + message: z.string().max(500, 'Message must be under 500 characters').optional(), +}); + +type BidFormData = z.infer; + +export interface BidSubmissionFormProps { + shipmentId: string; + hasAcceptedBid?: boolean; + hasUserBid?: boolean; + onSuccess?: () => void; +} + +export function BidSubmissionForm({ + shipmentId, + hasAcceptedBid = false, + hasUserBid = false, + onSuccess, +}: BidSubmissionFormProps) { + const queryClient = useQueryClient(); + const isDisabled = hasAcceptedBid || hasUserBid; + + const { + register, + handleSubmit, + reset, + formState: { errors, isSubmitting }, + } = useForm({ + resolver: zodResolver(bidSchema), + defaultValues: { currency: 'USD' }, + }); + + const bidMutation = useMutation({ + mutationFn: (data: BidFormData) => + apiClient('/bids', { + method: 'POST', + body: JSON.stringify({ ...data, shipmentId }), + }), + onSuccess: () => { + toast.success('Bid submitted successfully!'); + queryClient.invalidateQueries({ queryKey: ['bids', shipmentId] }); + reset(); + onSuccess?.(); + }, + onError: (err: Error) => { + toast.error(err.message || 'Failed to submit bid. Please try again.'); + }, + }); + + const onSubmit = (data: BidFormData) => { + bidMutation.mutate(data); + }; + + if (hasAcceptedBid) { + return ( + + +

+ This shipment already has an accepted bid. +

+
+
+ ); + } + + if (hasUserBid) { + return ( + + +

+ You have already submitted a bid for this shipment. +

+
+
+ ); + } + + return ( + + + Submit a Bid + + +
+
+
+ + + {errors.proposed_price && ( +

{errors.proposed_price.message}

+ )} +
+
+ + + {errors.currency && ( +

{errors.currency.message}

+ )} +
+
+ +
+ +