Skip to content

Commit e64b288

Browse files
committed
Bounty criteria: When/Then card layout with shared amount input
1 parent ebb05b2 commit e64b288

File tree

3 files changed

+130
-108
lines changed

3 files changed

+130
-108
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"use client";
2+
3+
import { isCurrencyAttribute } from "@/lib/api/workflows/utils";
4+
import { handleMoneyInputChange, handleMoneyKeyDown } from "@/lib/form-utils";
5+
import { InlineBadgePopoverContext } from "@/ui/shared/inline-badge-popover";
6+
import { cn } from "@dub/utils";
7+
import { useContext } from "react";
8+
import { useAddEditBountyForm } from "./bounty-form-context";
9+
10+
interface BountyAmountInputProps {
11+
name: "rewardAmount" | "performanceCondition.value";
12+
emptyValue?: null | undefined;
13+
}
14+
15+
export function BountyAmountInput({
16+
name,
17+
emptyValue = null,
18+
}: BountyAmountInputProps) {
19+
const { watch, register } = useAddEditBountyForm();
20+
const { setIsOpen } = useContext(InlineBadgePopoverContext);
21+
22+
const attribute =
23+
name === "performanceCondition.value"
24+
? watch("performanceCondition.attribute")
25+
: null;
26+
const isCurrency =
27+
name === "rewardAmount"
28+
? true
29+
: attribute
30+
? isCurrencyAttribute(attribute)
31+
: false;
32+
33+
return (
34+
<div className="relative rounded-md shadow-sm">
35+
{isCurrency && (
36+
<span className="absolute inset-y-0 left-0 flex items-center pl-1.5 text-sm text-neutral-400">
37+
$
38+
</span>
39+
)}
40+
<input
41+
className={cn(
42+
"block w-full rounded-md border-neutral-300 px-1.5 py-1 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:w-32 sm:text-sm",
43+
isCurrency ? "pl-4 pr-12" : "pr-7",
44+
)}
45+
{...register(name, {
46+
required: true,
47+
setValueAs: (value: string) => (value === "" ? emptyValue : +value),
48+
min: 0,
49+
onChange: handleMoneyInputChange,
50+
})}
51+
onKeyDown={(e) => {
52+
if (e.key === "Enter") {
53+
e.preventDefault();
54+
setIsOpen(false);
55+
return;
56+
}
57+
58+
handleMoneyKeyDown(e);
59+
}}
60+
/>
61+
{isCurrency && (
62+
<span className="absolute inset-y-0 right-0 flex items-center pr-1.5 text-sm text-neutral-400">
63+
USD
64+
</span>
65+
)}
66+
</div>
67+
);
68+
}

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

Lines changed: 56 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import {
55
ProgramSheetAccordionItem,
66
ProgramSheetAccordionTrigger,
77
} 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";
8+
import { RewardIconSquare } from "@/ui/partners/rewards/reward-icon-square";
9+
import { InlineBadgePopover } from "@/ui/shared/inline-badge-popover";
10+
import { CircleCheckFill, MoneyBills2, ToggleGroup } from "@dub/ui";
11+
import { cn, currencyFormatter } from "@dub/utils";
12+
import { BountyAmountInput } from "./bounty-amount-input";
1213
import { useAddEditBountyForm } from "./bounty-form-context";
1314
import { BountyLogic } from "./bounty-logic";
1415

@@ -29,23 +30,27 @@ export function BountyCriteriaSection({
2930
setRewardType,
3031
}: BountyCriteriaSectionProps) {
3132
const {
32-
control,
3333
register,
3434
watch,
3535
formState: { errors },
3636
} = useAddEditBountyForm();
3737

38-
const [type, rewardDescription] = watch(["type", "rewardDescription"]);
38+
const [type, rewardDescription, rewardAmount] = watch([
39+
"type",
40+
"rewardDescription",
41+
"rewardAmount",
42+
]);
43+
44+
const showWhenThenCards =
45+
(rewardType === "flat" || type === "performance") &&
46+
(type === "performance" ||
47+
(type === "submission" && rewardType === "flat"));
3948

4049
return (
4150
<ProgramSheetAccordionItem value="bounty-criteria">
4251
<ProgramSheetAccordionTrigger>Criteria</ProgramSheetAccordionTrigger>
4352
<ProgramSheetAccordionContent>
4453
<div className="space-y-6">
45-
<p className="text-content-default text-sm">
46-
Set the reward and completion criteria.
47-
</p>
48-
4954
{type === "submission" && (
5055
<ToggleGroup
5156
className="flex w-full items-center gap-1 rounded-md border border-neutral-200 bg-neutral-100 p-1"
@@ -57,42 +62,48 @@ export function BountyCriteriaSection({
5762
/>
5863
)}
5964

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-
/>
65+
{showWhenThenCards && (
66+
<div className="flex flex-col gap-0">
67+
<div className="border-border-subtle rounded-xl border bg-white shadow-sm">
68+
<div className="flex items-center gap-2.5 p-2.5">
69+
{type === "performance" ? (
70+
<BountyLogic />
71+
) : (
72+
<>
73+
<RewardIconSquare icon={CircleCheckFill} />
74+
<span className="text-content-emphasis text-sm font-medium leading-relaxed">
75+
When partner submits
76+
</span>
77+
</>
9478
)}
95-
/>
79+
</div>
80+
</div>
81+
82+
<div className="bg-border-subtle ml-6 h-4 w-px shrink-0" />
83+
84+
<div className="border-border-subtle rounded-xl border bg-white shadow-sm">
85+
<div className="flex items-center gap-2.5 p-2.5">
86+
<RewardIconSquare icon={MoneyBills2} />
87+
<span className="text-content-emphasis text-sm font-medium leading-relaxed">
88+
Then pay{" "}
89+
<InlineBadgePopover
90+
text={
91+
rewardAmount != null && !isNaN(rewardAmount)
92+
? currencyFormatter(rewardAmount * 100, {
93+
trailingZeroDisplay: "stripIfInteger",
94+
})
95+
: "$0"
96+
}
97+
invalid={
98+
rewardAmount == null ||
99+
isNaN(rewardAmount) ||
100+
rewardAmount < 0
101+
}
102+
>
103+
<BountyAmountInput name="rewardAmount" />
104+
</InlineBadgePopover>
105+
</span>
106+
</div>
96107
</div>
97108
</div>
98109
)}
@@ -129,15 +140,6 @@ export function BountyCriteriaSection({
129140
</div>
130141
)}
131142

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-
141143
{rewardType === "custom" && type === "submission" && (
142144
<div className="gap-4 rounded-lg bg-orange-50 px-4 py-2.5 text-center">
143145
<span className="text-sm font-medium text-orange-800">

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

Lines changed: 6 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,15 @@
22

33
import { PERFORMANCE_BOUNTY_SCOPE_ATTRIBUTES } from "@/lib/api/bounties/performance-bounty-scope-attributes";
44
import { isCurrencyAttribute } from "@/lib/api/workflows/utils";
5-
import { handleMoneyInputChange, handleMoneyKeyDown } from "@/lib/form-utils";
65
import { WORKFLOW_ATTRIBUTES } from "@/lib/zod/schemas/workflows";
76
import {
87
InlineBadgePopover,
9-
InlineBadgePopoverContext,
108
InlineBadgePopoverMenu,
119
} from "@/ui/shared/inline-badge-popover";
1210
import { Trophy } from "@dub/ui/icons";
1311
import { cn, currencyFormatter } from "@dub/utils";
14-
import { useContext } from "react";
1512
import { Controller } from "react-hook-form";
13+
import { BountyAmountInput } from "./bounty-amount-input";
1614
import { useAddEditBountyForm } from "./bounty-form-context";
1715

1816
export function BountyLogic({ className }: { className?: string }) {
@@ -24,12 +22,7 @@ export function BountyLogic({ className }: { className?: string }) {
2422
]);
2523

2624
return (
27-
<div
28-
className={cn(
29-
"flex w-full items-center gap-1.5 rounded-md border border-neutral-300 px-3 py-2",
30-
className,
31-
)}
32-
>
25+
<div className={cn("flex w-full items-center gap-1.5", className)}>
3326
<div className="flex size-7 shrink-0 items-center justify-center rounded-md bg-neutral-100">
3427
<Trophy className="size-4 text-neutral-800" />
3528
</div>
@@ -98,55 +91,14 @@ export function BountyLogic({ className }: { className?: string }) {
9891
}
9992
invalid={!value}
10093
>
101-
<ValueInput />
94+
<BountyAmountInput
95+
name="performanceCondition.value"
96+
emptyValue={undefined}
97+
/>
10298
</InlineBadgePopover>
10399
</>
104100
)}
105101
</span>
106102
</div>
107103
);
108104
}
109-
110-
function ValueInput() {
111-
const { watch, register } = useAddEditBountyForm();
112-
const { setIsOpen } = useContext(InlineBadgePopoverContext);
113-
114-
const attribute = watch("performanceCondition.attribute");
115-
const isCurrency = isCurrencyAttribute(attribute);
116-
117-
return (
118-
<div className="relative rounded-md shadow-sm">
119-
{isCurrency && (
120-
<span className="absolute inset-y-0 left-0 flex items-center pl-1.5 text-sm text-neutral-400">
121-
$
122-
</span>
123-
)}
124-
<input
125-
className={cn(
126-
"block w-full rounded-md border-neutral-300 px-1.5 py-1 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:w-32 sm:text-sm",
127-
isCurrency ? "pl-4 pr-12" : "pr-7",
128-
)}
129-
{...register("performanceCondition.value", {
130-
required: true,
131-
setValueAs: (value: string) => (value === "" ? undefined : +value),
132-
min: 0,
133-
onChange: handleMoneyInputChange,
134-
})}
135-
onKeyDown={(e) => {
136-
if (e.key === "Enter") {
137-
e.preventDefault();
138-
setIsOpen(false);
139-
return;
140-
}
141-
142-
handleMoneyKeyDown(e);
143-
}}
144-
/>
145-
{isCurrency && (
146-
<span className="absolute inset-y-0 right-0 flex items-center pr-1.5 text-sm text-neutral-400">
147-
USD
148-
</span>
149-
)}
150-
</div>
151-
);
152-
}

0 commit comments

Comments
 (0)