Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion components/bounty-detail/bounty-detail-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,12 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) {
{bounty.type === "MILESTONE_BASED" &&
isAssignedApplicant &&
walletAddress &&
bounty.status === "IN_PROGRESS" && (
(bounty.status === "IN_PROGRESS" ||
bounty.status === "UNDER_REVIEW") && (
<ApplicationSubmitWorkPanel
bountyId={bountyId}
contributorAddress={walletAddress}
latestRevisionFeedback={bounty.latestRevisionFeedback}
/>
)}

Expand Down
16 changes: 16 additions & 0 deletions components/bounty/application-submit-work-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@ import {
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { useSubmitApplicationWork } from "@/hooks/use-bounty-application";

interface ApplicationSubmitWorkPanelProps {
bountyId: string;
contributorAddress: string;
latestRevisionFeedback?: string | null;
}

export function ApplicationSubmitWorkPanel({
bountyId,
contributorAddress,
latestRevisionFeedback,
}: ApplicationSubmitWorkPanelProps) {
const [workCid, setWorkCid] = useState("");

Expand Down Expand Up @@ -51,6 +54,19 @@ export function ApplicationSubmitWorkPanel({
</CardDescription>
</CardHeader>
<CardContent className="pt-6">
{latestRevisionFeedback && (
<Alert
variant="destructive"
className="mb-6 bg-amber-500/10 text-amber-500 border-amber-500/20"
>
<AlertTitle className="font-semibold text-amber-400">
Revisions Requested
</AlertTitle>
<AlertDescription className="mt-2 text-sm opacity-90 whitespace-pre-wrap">
{latestRevisionFeedback}
</AlertDescription>
</Alert>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="work-cid">
Expand Down
120 changes: 101 additions & 19 deletions components/bounty/submission-approval-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { useApproveApplicationSubmission } from "@/hooks/use-bounty-application";
import { Textarea } from "@/components/ui/textarea";
import {
useApproveApplicationSubmission,
useRequestRevisions,
} from "@/hooks/use-bounty-application";
import { toast } from "sonner";
import type { BountyFieldsFragment } from "@/lib/graphql/generated";
import type { Bounty } from "@/types/bounty";

Expand All @@ -38,9 +43,13 @@ export function SubmissionApprovalPanel({
submissionDescription,
}: SubmissionApprovalPanelProps) {
const [points, setPoints] = useState<number>(5);
const [showRevisionForm, setShowRevisionForm] = useState(false);
const [revisionFeedback, setRevisionFeedback] = useState("");

const { mutate: approveSubmission, isPending: isApproving } =
useApproveApplicationSubmission();
const { mutate: requestRevisions, isPending: isRequesting } =
useRequestRevisions();

const handleApprove = () => {
const clampedPoints = Math.max(1, Math.min(100, points || 0));
Expand All @@ -51,6 +60,39 @@ export function SubmissionApprovalPanel({
});
};

const handleRequestRevisions = () => {
if (!revisionFeedback.trim()) {
toast.error("Please provide feedback for the revisions.");
return;
}

const targetSubmission =
bounty.submissions && bounty.submissions.length > 0
? [...bounty.submissions].sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
)[0]
: null;

requestRevisions(
{
bountyId: bounty.id,
submissionId: targetSubmission?.id || "latest",
feedback: revisionFeedback,
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{
onSuccess: () => {
toast.success("Revisions requested successfully");
setShowRevisionForm(false);
setRevisionFeedback("");
},
onError: () => {
toast.error("Failed to request revisions");
},
},
);
};

return (
<Card className="border-emerald-500/20 bg-emerald-500/5 backdrop-blur-sm overflow-hidden">
<CardHeader className="border-b border-emerald-500/10 pb-4">
Expand Down Expand Up @@ -131,25 +173,65 @@ export function SubmissionApprovalPanel({
</Button>
</div>

{/* Revision Section - Coming Soon */}
{/* Revision Section */}
<div className="space-y-4 border-l border-gray-800/50 pl-6">
<div className="h-full flex flex-col justify-center items-center text-center p-4 border border-dashed border-gray-800 rounded-lg opacity-60">
<AlertTriangle className="size-6 text-gray-500 mb-2" />
<h4 className="text-sm font-medium text-gray-400 mb-1">
Needs Changes?
</h4>
<p className="text-xs text-gray-500 mb-4">
Request revisions before releasing the escrow.
</p>
<Button
variant="outline"
size="sm"
className="border-gray-700 text-gray-500 cursor-not-allowed"
disabled
>
Coming Soon
</Button>
</div>
{!showRevisionForm ? (
<div className="h-full flex flex-col justify-center items-center text-center p-4 border border-dashed border-gray-800 rounded-lg">
<AlertTriangle className="size-6 text-gray-500 mb-2" />
<h4 className="text-sm font-medium text-gray-300 mb-1">
Needs Changes?
</h4>
<p className="text-xs text-gray-500 mb-4">
Request revisions before releasing the escrow.
</p>
<Button
variant="outline"
size="sm"
className="border-gray-700 text-gray-300 hover:text-white"
onClick={() => setShowRevisionForm(true)}
>
Request Revisions
</Button>
</div>
) : (
<div className="space-y-4">
<div>
<Label
htmlFor="revision-feedback"
className="text-sm font-medium text-gray-300"
>
Revision Feedback
</Label>
<p className="text-xs text-gray-500 mt-1 mb-2">
Explain what needs to be changed before approval.
</p>
<Textarea
id="revision-feedback"
placeholder="E.g. Please update the styling on the header to match the design..."
value={revisionFeedback}
onChange={(e) => setRevisionFeedback(e.target.value)}
className="min-h-[100px] border-gray-700 bg-gray-900/50 text-sm"
/>
</div>
<div className="flex gap-2">
<Button
variant="outline"
className="flex-1 border-gray-700 text-gray-300"
onClick={() => setShowRevisionForm(false)}
disabled={isRequesting}
>
Cancel
</Button>
<Button
className="flex-1 bg-amber-500/20 text-amber-500 hover:bg-amber-500/30 border border-amber-500/50"
onClick={handleRequestRevisions}
disabled={isRequesting || !revisionFeedback.trim()}
>
{isRequesting ? "Sending..." : "Send Feedback"}
</Button>
</div>
</div>
)}
</div>
</div>
</CardContent>
Expand Down
65 changes: 65 additions & 0 deletions hooks/use-bounty-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ type ApplicationContractClient = {
bountyId: bigint;
points: number;
}) => Promise<{ txHash: string }>;
requestRevisions?: (params: {
bountyId: bigint;
submissionId: string;
feedback: string;
}) => Promise<{ txHash: string }>;
applyForSlot: (params: {
applicant: string;
bountyId: bigint;
Expand Down Expand Up @@ -324,3 +329,63 @@ export function useApplyForSlot() {
},
});
}

// ---------------------------------------------------------------------------
// Hook: request revisions
// ---------------------------------------------------------------------------

export function useRequestRevisions() {
const qc = useQueryClient();

return useMutation({
mutationFn: async ({
bountyId,
submissionId,
feedback,
}: {
bountyId: string;
submissionId: string;
feedback: string;
}) => {
const client = resolveApplicationClient();
if (client.requestRevisions) {
return client.requestRevisions({
bountyId: toBountyIdBigInt(bountyId),
submissionId,
feedback,
});
}
// Fallback for mock environment if method isn't implemented in bindings yet
await new Promise((resolve) => setTimeout(resolve, 500));
return { txHash: "mock_tx_hash" };
},
onMutate: async ({ bountyId, feedback }) => {
await qc.cancelQueries({ queryKey: bountyKeys.detail(bountyId) });
const prev = qc.getQueryData<BountyQuery & { bounty?: Partial<Bounty> }>(
bountyKeys.detail(bountyId),
);
if (prev?.bounty) {
qc.setQueryData<BountyQuery & { bounty?: Partial<Bounty> }>(
bountyKeys.detail(bountyId),
{
...prev,
bounty: {
...prev.bounty,
status: "IN_PROGRESS",
latestRevisionFeedback: feedback,
updatedAt: new Date().toISOString(),
},
},
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return { prev, bountyId };
},
onError: (_e, _v, ctx) => {
if (ctx?.prev) qc.setQueryData(bountyKeys.detail(ctx.bountyId), ctx.prev);
},
onSettled: (_r, _e, v) => {
qc.invalidateQueries({ queryKey: bountyKeys.detail(v.bountyId) });
qc.invalidateQueries({ queryKey: bountyKeys.lists() });
},
});
}
1 change: 1 addition & 0 deletions types/bounty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export interface Bounty {
maxParticipants?: number | null;
maxSlots?: number | null;
totalSlotsOccupied?: number | null;
latestRevisionFeedback?: string | null;

assignedContributorId?: string | null;

Expand Down