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
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,34 @@ const DEFAULT_REWARD_TYPES = [
{
key: "sale",
label: "Sale",
description: "For sales and subscriptions",
description: "When revenue is generated",
mostCommon: true,
},
{
key: "lead",
label: "Lead",
description: "For sign ups and leads",
description: "For sign ups and demos",
mostCommon: false,
},
] as const;

const COMMISSION_STRUCTURE_DESCRIPTIONS: Record<string, string> = {
"one-off": "For referrals and fixed payouts",
recurring: "For ongoing revenue share",
};

const PAYOUT_MODELS = [
{
key: "percentage",
label: "Percentage",
description: "Share of the revenue",
mostCommon: true,
},
{
key: "flat",
label: "Flat",
description: "Fixed amount per conversion",
mostCommon: false,
},
] as const;

Expand Down Expand Up @@ -111,50 +133,60 @@ export function Form() {
</div>

<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
{DEFAULT_REWARD_TYPES.map(({ key, label, description }) => {
const isSelected = key === defaultRewardType;
{DEFAULT_REWARD_TYPES.map(
({ key, label, description, mostCommon }) => {
const isSelected = key === defaultRewardType;

return (
<label
key={key}
className={cn(
"relative flex w-full cursor-pointer items-start gap-0.5 rounded-md border border-neutral-200 bg-white p-3 text-neutral-600 hover:bg-neutral-50",
"transition-all duration-150",
isSelected &&
"border-black bg-neutral-50 text-neutral-900 ring-1 ring-black",
)}
>
<input
type="radio"
value={key}
className="hidden"
checked={isSelected}
onChange={() => {
setValue("defaultRewardType", key, { shouldDirty: true });
return (
<div key={key} className="flex flex-col items-center">
<label
className={cn(
"relative flex w-full cursor-pointer items-start gap-0.5 rounded-md border border-neutral-200 bg-white p-3 text-neutral-600 hover:bg-neutral-50",
"transition-all duration-150",
isSelected &&
"border-black bg-neutral-50 text-neutral-900 ring-1 ring-black",
)}
>
<input
type="radio"
value={key}
className="hidden"
checked={isSelected}
onChange={() => {
setValue("defaultRewardType", key, {
shouldDirty: true,
});

if (key === "lead") {
setValue("type", "flat", { shouldDirty: true });
setValue("maxDuration", 0, { shouldDirty: true });
}
}}
/>
<div className="flex grow flex-col text-sm">
<span className="text-sm font-semibold text-neutral-900">
{label}
</span>
<span className="text-sm font-normal text-neutral-600">
{description}
</span>
</div>
<CircleCheckFill
className={cn(
"-mr-px -mt-px flex size-4 scale-75 items-center justify-center rounded-full opacity-0 transition-[transform,opacity] duration-150",
isSelected && "scale-100 opacity-100",
if (key === "lead") {
setValue("type", "flat", { shouldDirty: true });
setValue("maxDuration", 0, { shouldDirty: true });
}
}}
/>
<div className="flex grow flex-col text-sm">
<span className="text-sm font-semibold text-neutral-900">
{label}
</span>
<span className="text-sm font-normal text-neutral-600">
{description}
</span>
</div>
<CircleCheckFill
className={cn(
"-mr-px -mt-px flex size-4 scale-75 items-center justify-center rounded-full opacity-0 transition-[transform,opacity] duration-150",
isSelected && "scale-100 opacity-100",
)}
/>
</label>
{mostCommon && (
<span className="mt-1.5 text-xs text-neutral-400">
Most common
</span>
)}
/>
</label>
);
})}
</div>
);
},
)}
</div>
</div>

Expand All @@ -170,53 +202,61 @@ export function Form() {
</div>

<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
{COMMISSION_TYPES.map(({ value, label, description }) => {
{COMMISSION_TYPES.map(({ value, label }) => {
const isSelected = value === commissionStructure;

return (
<label
key={value}
className={cn(
"relative flex w-full cursor-pointer items-start gap-0.5 rounded-md border border-neutral-200 bg-white p-3 text-neutral-600 hover:bg-neutral-50",
"transition-all duration-150",
isSelected &&
"border-black bg-neutral-50 text-neutral-900 ring-1 ring-black",
)}
>
<input
type="radio"
value={value}
className="hidden"
checked={isSelected}
onChange={(e) => {
if (value === "one-off") {
setCommissionStructure("one-off");
setValue("maxDuration", 0, { shouldValidate: true });
}

if (value === "recurring") {
setCommissionStructure("recurring");
setValue("maxDuration", 12, {
shouldValidate: true,
});
}
}}
/>
<div className="flex grow flex-col text-sm">
<span className="text-sm font-semibold text-neutral-900">
{label}
</span>
<span className="text-sm font-normal text-neutral-600">
{description}
</span>
</div>
<CircleCheckFill
<div key={value} className="flex flex-col items-center">
<label
className={cn(
"-mr-px -mt-px flex size-4 scale-75 items-center justify-center rounded-full opacity-0 transition-[transform,opacity] duration-150",
isSelected && "scale-100 opacity-100",
"relative flex w-full cursor-pointer items-start gap-0.5 rounded-md border border-neutral-200 bg-white p-3 text-neutral-600 hover:bg-neutral-50",
"transition-all duration-150",
isSelected &&
"border-black bg-neutral-50 text-neutral-900 ring-1 ring-black",
)}
/>
</label>
>
<input
type="radio"
value={value}
className="hidden"
checked={isSelected}
onChange={() => {
if (value === "one-off") {
setCommissionStructure("one-off");
setValue("maxDuration", 0, {
shouldValidate: true,
});
}

if (value === "recurring") {
setCommissionStructure("recurring");
setValue("maxDuration", 12, {
shouldValidate: true,
});
}
}}
/>
<div className="flex grow flex-col text-sm">
<span className="text-sm font-semibold text-neutral-900">
{label}
</span>
<span className="text-sm font-normal text-neutral-600">
{COMMISSION_STRUCTURE_DESCRIPTIONS[value]}
</span>
</div>
<CircleCheckFill
className={cn(
"-mr-px -mt-px flex size-4 scale-75 items-center justify-center rounded-full opacity-0 transition-[transform,opacity] duration-150",
isSelected && "scale-100 opacity-100",
)}
/>
</label>
{value === "one-off" && (
<span className="mt-1.5 text-xs text-neutral-400">
Most common
</span>
)}
</div>
);
})}
</div>
Expand Down Expand Up @@ -267,23 +307,61 @@ export function Form() {
</div>

{defaultRewardType === "sale" && (
<div>
<label className="text-sm font-medium text-neutral-800">
Reward structure
</label>
<select
{...register("type")}
className="mt-2 block w-full rounded-md border border-neutral-300 bg-white py-2 pl-3 pr-10 text-sm text-neutral-900 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500"
>
<option value="flat">Flat</option>
<option value="percentage">Percentage</option>
</select>
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
{PAYOUT_MODELS.map(
({ key, label, description, mostCommon }) => {
const isSelected = key === type;

return (
<div key={key} className="flex flex-col items-center">
<label
className={cn(
"relative flex w-full cursor-pointer items-start gap-0.5 rounded-md border border-neutral-200 bg-white p-3 text-neutral-600 hover:bg-neutral-50",
"transition-all duration-150",
isSelected &&
"border-black bg-neutral-50 text-neutral-900 ring-1 ring-black",
)}
>
<input
type="radio"
value={key}
className="hidden"
checked={isSelected}
onChange={() =>
setValue("type", key, { shouldDirty: true })
}
/>
<div className="flex grow flex-col text-sm">
<span className="text-sm font-semibold text-neutral-900">
{label}
</span>
<span className="text-sm font-normal text-neutral-600">
{description}
</span>
</div>
<CircleCheckFill
className={cn(
"-mr-px -mt-px flex size-4 scale-75 items-center justify-center rounded-full opacity-0 transition-[transform,opacity] duration-150",
isSelected && "scale-100 opacity-100",
)}
/>
</label>
{mostCommon && (
<span className="mt-1.5 text-xs text-neutral-400">
Most common
</span>
)}
</div>
);
},
)}
</div>
)}

<div>
<label className="text-sm font-medium text-neutral-800">
Reward amount {defaultRewardType != "sale" ? "per lead" : ""}
{type === "percentage" ? "Percentage" : "Amount"} per{" "}
{defaultRewardType}
</label>
<div className="relative mt-2 rounded-md shadow-sm">
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-sm text-neutral-400">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Form } from "./form";

export default async function ProgramOnboardingRewardsPage() {
return (
<StepPage title="Configure rewards">
<StepPage title="Create default reward">
<Form />
</StepPage>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@ import Link from "next/link";
import { useParams } from "next/navigation";
import { useEffect, useState } from "react";

const REWARD_EVENT_DESCRIPTIONS: Record<
EventType,
{ title: string; description: string }
> = {
sale: {
title: "Sale reward",
description: "Reward when revenue is generated.",
},
lead: {
title: "Lead reward",
description: "Reward for sign ups or demos.",
},
click: {
title: "Click reward",
description: "Reward for traffic and reach.",
},
};

export function GroupRewards() {
const { group, loading } = useGroup();
const { searchParams } = useRouterStuff();
Expand Down Expand Up @@ -157,18 +175,23 @@ const RewardItem = ({
</div>
<div className="flex flex-1 flex-col justify-between gap-y-4 md:flex-row md:items-center">
<div className="flex items-center gap-2">
<span className="text-sm font-normal">
{reward ? (
{reward ? (
<span className="text-sm font-normal">
<ProgramRewardDescription
reward={reward}
amountClassName="text-blue-600"
/>
) : (
<span className="text-sm font-normal text-neutral-600">
No {event} reward configured
</span>
) : (
<div className="flex flex-col">
<span className="text-sm font-medium text-neutral-900">
{REWARD_EVENT_DESCRIPTIONS[event].title}
</span>
)}
</span>
<span className="text-sm font-normal text-neutral-500">
{REWARD_EVENT_DESCRIPTIONS[event].description}
</span>
</div>
)}
</div>

{reward ? (
Expand Down
Loading
Loading