Skip to content

Commit ebb05b2

Browse files
devkirancursoragent
andcommitted
Split bounty add/edit form into modular add-edit-bounty directory
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 6e5b184 commit ebb05b2

File tree

10 files changed

+183
-161
lines changed

10 files changed

+183
-161
lines changed

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx renamed to apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/add-edit-bounty-sheet.tsx

Lines changed: 12 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,15 @@ import { mutatePrefix } from "@/lib/swr/mutate";
1010
import { useApiMutation } from "@/lib/swr/use-api-mutation";
1111
import useProgram from "@/lib/swr/use-program";
1212
import useWorkspace from "@/lib/swr/use-workspace";
13-
import { BountyProps } from "@/lib/types";
14-
import {
15-
bountyPerformanceConditionSchema,
16-
createBountySchema,
17-
} from "@/lib/zod/schemas/bounties";
18-
import { BountyLogic } from "@/ui/partners/bounties/bounty-logic";
13+
import { BountyFormData, BountyProps } from "@/lib/types";
14+
import { bountyPerformanceConditionSchema } from "@/lib/zod/schemas/bounties";
1915
import { GroupsMultiSelect } from "@/ui/partners/groups/groups-multi-select";
2016
import {
2117
ProgramSheetAccordion,
2218
ProgramSheetAccordionContent,
2319
ProgramSheetAccordionItem,
2420
ProgramSheetAccordionTrigger,
2521
} from "@/ui/partners/program-sheet-accordion";
26-
import { AmountInput } from "@/ui/shared/amount-input";
2722
import { X } from "@/ui/shared/icons";
2823
import { MaxCharactersCounter } from "@/ui/shared/max-characters-counter";
2924
import {
@@ -38,29 +33,19 @@ import {
3833
Sheet,
3934
SmartDateTimePicker,
4035
Switch,
41-
ToggleGroup,
4236
useRouterStuff,
4337
} from "@dub/ui";
4438
import { cn, formatDate } from "@dub/utils";
4539
import { Dispatch, SetStateAction, useMemo, useState } from "react";
46-
import {
47-
Controller,
48-
FormProvider,
49-
useForm,
50-
useFormContext,
51-
} from "react-hook-form";
40+
import { Controller, FormProvider, useForm } from "react-hook-form";
5241
import { toast } from "sonner";
53-
import * as z from "zod/v4";
42+
import { BountyCriteriaSection } from "./bounty-criteria-section";
5443
import { useConfirmCreateBountyModal } from "./confirm-create-bounty-modal";
5544

56-
type BountySheetProps = {
45+
interface BountySheetProps {
5746
setIsOpen: Dispatch<SetStateAction<boolean>>;
5847
bounty?: BountyProps;
59-
};
60-
61-
type FormData = z.infer<typeof createBountySchema>;
62-
63-
export const useAddEditBountyForm = () => useFormContext<FormData>();
48+
}
6449

6550
const BOUNTY_TYPES: CardSelectorOption[] = [
6651
{
@@ -75,18 +60,6 @@ const BOUNTY_TYPES: CardSelectorOption[] = [
7560
},
7661
];
7762

78-
// Only valid for submission bounties
79-
const REWARD_TYPES = [
80-
{
81-
value: "flat",
82-
label: "Flat rate",
83-
},
84-
{
85-
value: "custom",
86-
label: "Custom",
87-
},
88-
];
89-
9063
const ACCORDION_ITEMS = [
9164
"bounty-type",
9265
"bounty-details",
@@ -146,7 +119,7 @@ function BountySheetContent({ setIsOpen, bounty }: BountySheetProps) {
146119
bounty ? (bounty.rewardAmount ? "flat" : "custom") : "flat",
147120
);
148121

149-
const form = useForm<FormData>({
122+
const form = useForm<BountyFormData>({
150123
defaultValues: {
151124
name: bounty?.name || undefined,
152125
description: bounty?.description || undefined,
@@ -645,7 +618,7 @@ function BountySheetContent({ setIsOpen, bounty }: BountySheetProps) {
645618
<CardSelector
646619
options={BOUNTY_TYPES}
647620
value={watch("type")}
648-
onChange={(value: FormData["type"]) =>
621+
onChange={(value: BountyFormData["type"]) =>
649622
setValue("type", value)
650623
}
651624
name="bounty-type"
@@ -825,7 +798,6 @@ function BountySheetContent({ setIsOpen, bounty }: BountySheetProps) {
825798
</div>
826799
</div>
827800
</div>
828-
829801
</>
830802
)}
831803

@@ -884,122 +856,10 @@ function BountySheetContent({ setIsOpen, bounty }: BountySheetProps) {
884856
</ProgramSheetAccordionContent>
885857
</ProgramSheetAccordionItem>
886858

887-
<ProgramSheetAccordionItem value="bounty-criteria">
888-
<ProgramSheetAccordionTrigger>
889-
Criteria
890-
</ProgramSheetAccordionTrigger>
891-
<ProgramSheetAccordionContent>
892-
<div className="space-y-6">
893-
<p className="text-content-default text-sm">
894-
Set the reward and completion criteria.
895-
</p>
896-
897-
{type === "submission" && (
898-
<ToggleGroup
899-
className="flex w-full items-center gap-1 rounded-md border border-neutral-200 bg-neutral-100 p-1"
900-
optionClassName="h-8 flex items-center justify-center rounded-md flex-1 text-sm"
901-
indicatorClassName="bg-white border-none rounded-md"
902-
options={REWARD_TYPES}
903-
selected={rewardType}
904-
selectAction={(id: RewardType) => setRewardType(id)}
905-
/>
906-
)}
907-
908-
{(rewardType === "flat" || type === "performance") && (
909-
<div>
910-
<label
911-
htmlFor="rewardAmount"
912-
className="text-sm font-medium text-neutral-800"
913-
>
914-
Reward
915-
</label>
916-
<div className="mt-2">
917-
<Controller
918-
name="rewardAmount"
919-
control={control}
920-
rules={{
921-
required: true,
922-
min: 0,
923-
}}
924-
render={({ field }) => (
925-
<AmountInput
926-
{...field}
927-
id="rewardAmount"
928-
amountType="flat"
929-
placeholder="200"
930-
error={errors.rewardAmount?.message}
931-
value={
932-
field.value == null || isNaN(field.value)
933-
? ""
934-
: field.value
935-
}
936-
onChange={(e) => {
937-
const val = e.target.value;
938-
939-
field.onChange(
940-
val === "" ? null : parseFloat(val),
941-
);
942-
}}
943-
/>
944-
)}
945-
/>
946-
</div>
947-
</div>
948-
)}
949-
950-
{rewardType === "custom" && type === "submission" && (
951-
<div>
952-
<label
953-
htmlFor="rewardDescription"
954-
className="text-sm font-medium text-neutral-800"
955-
>
956-
Reward
957-
</label>
958-
<div className="mt-2">
959-
<input
960-
id="rewardDescription"
961-
type="text"
962-
maxLength={100}
963-
className={cn(
964-
"block w-full rounded-md border-neutral-300 px-3 py-2 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm",
965-
errors.rewardDescription &&
966-
"border-red-600 focus:border-red-500 focus:ring-red-600",
967-
)}
968-
placeholder="Earn an additional 10% if you hit your revenue goal"
969-
{...register("rewardDescription", {
970-
setValueAs: (value) =>
971-
value === "" ? null : value,
972-
})}
973-
/>
974-
<div className="mt-1 text-left">
975-
<span className="text-xs text-neutral-400">
976-
{rewardDescription?.length || 0}/100
977-
</span>
978-
</div>
979-
</div>
980-
</div>
981-
)}
982-
983-
{type === "performance" && (
984-
<div>
985-
<span className="text-sm font-medium text-neutral-800">
986-
Logic
987-
</span>
988-
<BountyLogic className="mt-2" />
989-
</div>
990-
)}
991-
992-
{rewardType === "custom" && type === "submission" && (
993-
<div className="gap-4 rounded-lg bg-orange-50 px-4 py-2.5 text-center">
994-
<span className="text-sm font-medium text-orange-800">
995-
When reviewing these submissions, a custom reward
996-
amount will be required to approve.
997-
</span>
998-
</div>
999-
)}
1000-
</div>
1001-
</ProgramSheetAccordionContent>
1002-
</ProgramSheetAccordionItem>
859+
<BountyCriteriaSection
860+
rewardType={rewardType}
861+
setRewardType={setRewardType}
862+
/>
1003863

1004864
{type === "submission" && (
1005865
<ProgramSheetAccordionItem value="submission-requirements">
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"use client";
2+
3+
import {
4+
ProgramSheetAccordionContent,
5+
ProgramSheetAccordionItem,
6+
ProgramSheetAccordionTrigger,
7+
} from "@/ui/partners/program-sheet-accordion";
8+
import { AmountInput } from "@/ui/shared/amount-input";
9+
import { ToggleGroup } from "@dub/ui";
10+
import { cn } from "@dub/utils";
11+
import { Controller } from "react-hook-form";
12+
import { useAddEditBountyForm } from "./bounty-form-context";
13+
import { BountyLogic } from "./bounty-logic";
14+
15+
const REWARD_TYPES = [
16+
{ value: "flat", label: "Flat rate" },
17+
{ value: "custom", label: "Custom" },
18+
];
19+
20+
type RewardType = "flat" | "custom";
21+
22+
interface BountyCriteriaSectionProps {
23+
rewardType: RewardType;
24+
setRewardType: (type: RewardType) => void;
25+
}
26+
27+
export function BountyCriteriaSection({
28+
rewardType,
29+
setRewardType,
30+
}: BountyCriteriaSectionProps) {
31+
const {
32+
control,
33+
register,
34+
watch,
35+
formState: { errors },
36+
} = useAddEditBountyForm();
37+
38+
const [type, rewardDescription] = watch(["type", "rewardDescription"]);
39+
40+
return (
41+
<ProgramSheetAccordionItem value="bounty-criteria">
42+
<ProgramSheetAccordionTrigger>Criteria</ProgramSheetAccordionTrigger>
43+
<ProgramSheetAccordionContent>
44+
<div className="space-y-6">
45+
<p className="text-content-default text-sm">
46+
Set the reward and completion criteria.
47+
</p>
48+
49+
{type === "submission" && (
50+
<ToggleGroup
51+
className="flex w-full items-center gap-1 rounded-md border border-neutral-200 bg-neutral-100 p-1"
52+
optionClassName="h-8 flex items-center justify-center rounded-md flex-1 text-sm"
53+
indicatorClassName="bg-white border-none rounded-md"
54+
options={REWARD_TYPES}
55+
selected={rewardType}
56+
selectAction={(id: RewardType) => setRewardType(id)}
57+
/>
58+
)}
59+
60+
{(rewardType === "flat" || type === "performance") && (
61+
<div>
62+
<label
63+
htmlFor="rewardAmount"
64+
className="text-sm font-medium text-neutral-800"
65+
>
66+
Reward
67+
</label>
68+
<div className="mt-2">
69+
<Controller
70+
name="rewardAmount"
71+
control={control}
72+
rules={{
73+
required: true,
74+
min: 0,
75+
}}
76+
render={({ field }) => (
77+
<AmountInput
78+
{...field}
79+
id="rewardAmount"
80+
amountType="flat"
81+
placeholder="200"
82+
error={errors.rewardAmount?.message}
83+
value={
84+
field.value == null || isNaN(field.value)
85+
? ""
86+
: field.value
87+
}
88+
onChange={(e) => {
89+
const val = e.target.value;
90+
91+
field.onChange(val === "" ? null : parseFloat(val));
92+
}}
93+
/>
94+
)}
95+
/>
96+
</div>
97+
</div>
98+
)}
99+
100+
{rewardType === "custom" && type === "submission" && (
101+
<div>
102+
<label
103+
htmlFor="rewardDescription"
104+
className="text-sm font-medium text-neutral-800"
105+
>
106+
Reward
107+
</label>
108+
<div className="mt-2">
109+
<input
110+
id="rewardDescription"
111+
type="text"
112+
maxLength={100}
113+
className={cn(
114+
"block w-full rounded-md border-neutral-300 px-3 py-2 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm",
115+
errors.rewardDescription &&
116+
"border-red-600 focus:border-red-500 focus:ring-red-600",
117+
)}
118+
placeholder="Earn an additional 10% if you hit your revenue goal"
119+
{...register("rewardDescription", {
120+
setValueAs: (value) => (value === "" ? null : value),
121+
})}
122+
/>
123+
<div className="mt-1 text-left">
124+
<span className="text-xs text-neutral-400">
125+
{rewardDescription?.length || 0}/100
126+
</span>
127+
</div>
128+
</div>
129+
</div>
130+
)}
131+
132+
{type === "performance" && (
133+
<div>
134+
<span className="text-sm font-medium text-neutral-800">
135+
Logic
136+
</span>
137+
<BountyLogic className="mt-2" />
138+
</div>
139+
)}
140+
141+
{rewardType === "custom" && type === "submission" && (
142+
<div className="gap-4 rounded-lg bg-orange-50 px-4 py-2.5 text-center">
143+
<span className="text-sm font-medium text-orange-800">
144+
When reviewing these submissions, a custom reward amount will be
145+
required to approve.
146+
</span>
147+
</div>
148+
)}
149+
</div>
150+
</ProgramSheetAccordionContent>
151+
</ProgramSheetAccordionItem>
152+
);
153+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"use client";
2+
3+
import { BountyFormData } from "@/lib/types";
4+
import { useFormContext } from "react-hook-form";
5+
6+
export const useAddEditBountyForm = () => useFormContext<BountyFormData>();

0 commit comments

Comments
 (0)