Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,15 @@ import { mutatePrefix } from "@/lib/swr/mutate";
import { useApiMutation } from "@/lib/swr/use-api-mutation";
import useProgram from "@/lib/swr/use-program";
import useWorkspace from "@/lib/swr/use-workspace";
import { BountyProps } from "@/lib/types";
import {
bountyPerformanceConditionSchema,
createBountySchema,
} from "@/lib/zod/schemas/bounties";
import { BountyLogic } from "@/ui/partners/bounties/bounty-logic";
import { BountyFormData, BountyProps } from "@/lib/types";
import { bountyPerformanceConditionSchema } from "@/lib/zod/schemas/bounties";
import { GroupsMultiSelect } from "@/ui/partners/groups/groups-multi-select";
import {
ProgramSheetAccordion,
ProgramSheetAccordionContent,
ProgramSheetAccordionItem,
ProgramSheetAccordionTrigger,
} from "@/ui/partners/program-sheet-accordion";
import { AmountInput } from "@/ui/shared/amount-input";
import { X } from "@/ui/shared/icons";
import { MaxCharactersCounter } from "@/ui/shared/max-characters-counter";
import {
Expand All @@ -38,29 +33,19 @@ import {
Sheet,
SmartDateTimePicker,
Switch,
ToggleGroup,
useRouterStuff,
} from "@dub/ui";
import { cn, formatDate } from "@dub/utils";
import { Dispatch, SetStateAction, useMemo, useState } from "react";
import {
Controller,
FormProvider,
useForm,
useFormContext,
} from "react-hook-form";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { toast } from "sonner";
import * as z from "zod/v4";
import { BountyCriteriaSection } from "./bounty-criteria-section";
import { useConfirmCreateBountyModal } from "./confirm-create-bounty-modal";

type BountySheetProps = {
interface BountySheetProps {
setIsOpen: Dispatch<SetStateAction<boolean>>;
bounty?: BountyProps;
};

type FormData = z.infer<typeof createBountySchema>;

export const useAddEditBountyForm = () => useFormContext<FormData>();
}

const BOUNTY_TYPES: CardSelectorOption[] = [
{
Expand All @@ -75,21 +60,10 @@ const BOUNTY_TYPES: CardSelectorOption[] = [
},
];

// Only valid for submission bounties
const REWARD_TYPES = [
{
value: "flat",
label: "Flat rate",
},
{
value: "custom",
label: "Custom",
},
];

const ACCORDION_ITEMS = [
"bounty-type",
"bounty-details",
"bounty-criteria",
"submission-requirements",
"groups",
];
Expand Down Expand Up @@ -145,7 +119,7 @@ function BountySheetContent({ setIsOpen, bounty }: BountySheetProps) {
bounty ? (bounty.rewardAmount ? "flat" : "custom") : "flat",
);

const form = useForm<FormData>({
const form = useForm<BountyFormData>({
defaultValues: {
name: bounty?.name || undefined,
description: bounty?.description || undefined,
Expand Down Expand Up @@ -634,7 +608,7 @@ function BountySheetContent({ setIsOpen, bounty }: BountySheetProps) {
{!bounty && ( // cannot change type for existing bounties
<ProgramSheetAccordionItem value="bounty-type">
<ProgramSheetAccordionTrigger>
Bounty type
Type
</ProgramSheetAccordionTrigger>
<ProgramSheetAccordionContent>
<div className="space-y-4">
Expand All @@ -644,7 +618,7 @@ function BountySheetContent({ setIsOpen, bounty }: BountySheetProps) {
<CardSelector
options={BOUNTY_TYPES}
value={watch("type")}
onChange={(value: FormData["type"]) =>
onChange={(value: BountyFormData["type"]) =>
setValue("type", value)
}
name="bounty-type"
Expand All @@ -656,12 +630,12 @@ function BountySheetContent({ setIsOpen, bounty }: BountySheetProps) {

<ProgramSheetAccordionItem value="bounty-details">
<ProgramSheetAccordionTrigger>
Bounty details
Details
</ProgramSheetAccordionTrigger>
<ProgramSheetAccordionContent>
<div className="space-y-6">
<p className="text-content-default text-sm">
Set the schedule, reward, and additional details.
Set the schedule and additional details.
</p>

<AnimatedSizeContainer
Expand Down Expand Up @@ -824,105 +798,12 @@ function BountySheetContent({ setIsOpen, bounty }: BountySheetProps) {
</div>
</div>
</div>

<ToggleGroup
className="mt-2 flex w-full items-center gap-1 rounded-md border border-neutral-200 bg-neutral-100 p-1"
optionClassName="h-8 flex items-center justify-center rounded-md flex-1 text-sm"
indicatorClassName="bg-white border-none rounded-md"
options={REWARD_TYPES}
selected={rewardType}
selectAction={(id: RewardType) => setRewardType(id)}
/>
</>
)}

{(rewardType === "flat" || type === "performance") && (
<div>
<label
htmlFor="rewardAmount"
className="text-sm font-medium text-neutral-800"
>
Reward
</label>
<div className="mt-2">
<Controller
name="rewardAmount"
control={control}
rules={{
required: true,
min: 0,
}}
render={({ field }) => (
<AmountInput
{...field}
id="rewardAmount"
amountType="flat"
placeholder="200"
error={errors.rewardAmount?.message}
value={
field.value == null || isNaN(field.value)
? ""
: field.value
}
onChange={(e) => {
const val = e.target.value;

field.onChange(
val === "" ? null : parseFloat(val),
);
}}
/>
)}
/>
</div>
</div>
)}

{rewardType === "custom" && type === "submission" && (
<div>
<label
htmlFor="rewardDescription"
className="text-sm font-medium text-neutral-800"
>
Reward
</label>
<div className="mt-2">
<input
id="rewardDescription"
type="text"
maxLength={100}
className={cn(
"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",
errors.rewardDescription &&
"border-red-600 focus:border-red-500 focus:ring-red-600",
)}
placeholder="Earn an additional 10% if you hit your revenue goal"
{...register("rewardDescription", {
setValueAs: (value) =>
value === "" ? null : value,
})}
/>
<div className="mt-1 text-left">
<span className="text-xs text-neutral-400">
{rewardDescription?.length || 0}/100
</span>
</div>
</div>
</div>
)}

{type === "performance" && (
<div>
<span className="text-sm font-medium text-neutral-800">
Logic
</span>
<BountyLogic className="mt-2" />
</div>
)}

<div>
<label className="text-sm font-medium text-neutral-800">
Details
Description
<span className="ml-1 font-normal text-neutral-500">
(optional)
</span>
Expand Down Expand Up @@ -971,19 +852,15 @@ function BountySheetContent({ setIsOpen, bounty }: BountySheetProps) {
</div>
</div>
</div>

{rewardType === "custom" && (
<div className="gap-4 rounded-lg bg-orange-50 px-4 py-2.5 text-center">
<span className="text-sm font-medium text-orange-800">
When reviewing these submissions, a custom reward
amount will be required to approve.
</span>
</div>
)}
</div>
</ProgramSheetAccordionContent>
</ProgramSheetAccordionItem>

<BountyCriteriaSection
rewardType={rewardType}
setRewardType={setRewardType}
/>

{type === "submission" && (
<ProgramSheetAccordionItem value="submission-requirements">
<ProgramSheetAccordionTrigger>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use client";

import { isCurrencyAttribute } from "@/lib/api/workflows/utils";
import { handleMoneyInputChange, handleMoneyKeyDown } from "@/lib/form-utils";
import { InlineBadgePopoverContext } from "@/ui/shared/inline-badge-popover";
import { cn } from "@dub/utils";
import { useContext } from "react";
import { useAddEditBountyForm } from "./bounty-form-context";

interface BountyAmountInputProps {
name: "rewardAmount" | "performanceCondition.value";
emptyValue?: null | undefined;
}

export function BountyAmountInput({
name,
emptyValue = null,
}: BountyAmountInputProps) {
const { watch, register } = useAddEditBountyForm();
const { setIsOpen } = useContext(InlineBadgePopoverContext);

const attribute =
name === "performanceCondition.value"
? watch("performanceCondition.attribute")
: null;
const isCurrency =
name === "rewardAmount"
? true
: attribute
? isCurrencyAttribute(attribute)
: false;

return (
<div className="relative rounded-md shadow-sm">
{isCurrency && (
<span className="absolute inset-y-0 left-0 flex items-center pl-1.5 text-sm text-neutral-400">
$
</span>
)}
<input
className={cn(
"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",
isCurrency ? "pl-4 pr-12" : "pr-7",
)}
{...register(name, {
required: true,
setValueAs: (value: string) => (value === "" ? emptyValue : +value),
min: 0,
onChange: handleMoneyInputChange,
})}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
setIsOpen(false);
return;
}

handleMoneyKeyDown(e);
}}
/>
{isCurrency && (
<span className="absolute inset-y-0 right-0 flex items-center pr-1.5 text-sm text-neutral-400">
USD
</span>
)}
</div>
);
}
Loading