@@ -22,12 +22,16 @@ import { X } from "@/ui/shared/icons";
2222import { EventType , RewardStructure } from "@dub/prisma/client" ;
2323import {
2424 Button ,
25+ Gift ,
2526 MoneyBills2 ,
27+ Pen2 ,
2628 Sheet ,
29+ Tooltip ,
2730 TooltipContent ,
2831 useRouterStuff ,
2932} from "@dub/ui" ;
3033import { capitalize , cn , pluralize } from "@dub/utils" ;
34+ import { motion } from "framer-motion" ;
3135import { useAction } from "next-safe-action/hooks" ;
3236import {
3337 Dispatch ,
@@ -46,6 +50,7 @@ import { z } from "zod";
4650import {
4751 InlineBadgePopover ,
4852 InlineBadgePopoverContext ,
53+ InlineBadgePopoverInput ,
4954 InlineBadgePopoverMenu ,
5055} from "../../shared/inline-badge-popover" ;
5156import { usePartnersUpgradeModal } from "../partners-upgrade-modal" ;
@@ -103,6 +108,7 @@ function RewardSheetContent({
103108 defaultValuesSource ?. type === "flat"
104109 ? defaultValuesSource . amount / 100
105110 : defaultValuesSource ?. amount ,
111+ description : defaultValuesSource ?. description ?? null ,
106112 modifiers : defaultValuesSource ?. modifiers ?. map ( ( m ) => {
107113 const type = m . type === undefined ? defaultValuesSource ?. type : m . type ;
108114 const maxDuration =
@@ -131,13 +137,15 @@ function RewardSheetContent({
131137
132138 const { handleSubmit, watch, setValue, setError } = form ;
133139
134- const [ selectedEvent , amount , type , maxDuration , modifiers ] = watch ( [
135- "event" ,
136- "amount" ,
137- "type" ,
138- "maxDuration" ,
139- "modifiers" ,
140- ] ) ;
140+ const [ selectedEvent , amount , type , maxDuration , description , modifiers ] =
141+ watch ( [
142+ "event" ,
143+ "amount" ,
144+ "type" ,
145+ "maxDuration" ,
146+ "description" ,
147+ "modifiers" ,
148+ ] ) ;
141149
142150 const { executeAsync : createReward , isPending : isCreating } = useAction (
143151 createRewardAction ,
@@ -188,7 +196,7 @@ function RewardSheetContent({
188196
189197 useEffect ( ( ) => {
190198 if (
191- modifiers ?. length > 0 &&
199+ modifiers ?. length &&
192200 ! getPlanCapabilities ( plan ) . canUseAdvancedRewardLogic
193201 ) {
194202 setShowAdvancedUpsell ( true ) ;
@@ -308,83 +316,154 @@ function RewardSheetContent({
308316 < div className = "flex flex-1 flex-col overflow-y-auto p-6" >
309317 < RewardSheetCard
310318 title = {
311- < >
312- < RewardIconSquare icon = { MoneyBills2 } />
313- < span className = "leading-relaxed" >
314- Pay{ " " }
315- { selectedEvent === "sale" && (
316- < >
317- a{ " " }
318- < InlineBadgePopover text = { capitalize ( type ) } >
319- < InlineBadgePopoverMenu
320- selectedValue = { type }
321- onSelect = { ( value ) =>
322- setValue ( "type" , value as RewardStructure , {
323- shouldDirty : true ,
324- } )
325- }
326- items = { REWARD_TYPES }
327- />
328- </ InlineBadgePopover > { " " }
329- { type === "percentage" && "of " }
330- </ >
331- ) }
332- < InlineBadgePopover
333- text = {
334- ! isNaN ( amount )
335- ? constructRewardAmount ( {
336- amount : type === "flat" ? amount * 100 : amount ,
337- type,
338- maxDuration,
339- } )
340- : "amount"
341- }
342- invalid = { isNaN ( amount ) }
343- >
344- < AmountInput />
345- </ InlineBadgePopover > { " " }
346- per { selectedEvent }
347- { selectedEvent === "sale" && (
348- < >
349- { " " }
319+ < div className = "w-full" >
320+ < div className = "flex min-w-0 items-center justify-between" >
321+ < div className = "flex min-w-0 items-center gap-2.5" >
322+ < RewardIconSquare icon = { MoneyBills2 } />
323+ < span className = "leading-relaxed" >
324+ Pay{ " " }
325+ { selectedEvent === "sale" && (
326+ < >
327+ a{ " " }
328+ < InlineBadgePopover text = { capitalize ( type ) } >
329+ < InlineBadgePopoverMenu
330+ selectedValue = { type }
331+ onSelect = { ( value ) =>
332+ setValue ( "type" , value as RewardStructure , {
333+ shouldDirty : true ,
334+ } )
335+ }
336+ items = { REWARD_TYPES }
337+ />
338+ </ InlineBadgePopover > { " " }
339+ { type === "percentage" && "of " }
340+ </ >
341+ ) }
350342 < InlineBadgePopover
351343 text = {
352- maxDuration === 0
353- ? "one time"
354- : maxDuration === Infinity
355- ? "for the customer's lifetime"
356- : `for ${ maxDuration } ${ pluralize ( "month" , Number ( maxDuration ) ) } `
344+ ! isNaN ( amount )
345+ ? constructRewardAmount ( {
346+ amount : type === "flat" ? amount * 100 : amount ,
347+ type,
348+ maxDuration,
349+ } )
350+ : "amount"
357351 }
352+ invalid = { isNaN ( amount ) }
358353 >
359- < InlineBadgePopoverMenu
360- selectedValue = { maxDuration ?. toString ( ) }
361- onSelect = { ( value ) =>
362- setValue ( "maxDuration" , Number ( value ) , {
363- shouldDirty : true ,
364- } )
365- }
366- items = { [
367- {
368- text : "one time" ,
369- value : "0" ,
370- } ,
371- ...RECURRING_MAX_DURATIONS . filter (
372- ( v ) => v !== 0 && v !== 1 , // filter out one-time and 1-month intervals (we only use 1-month for discounts)
373- ) . map ( ( v ) => ( {
374- text : `for ${ v } ${ pluralize ( "month" , Number ( v ) ) } ` ,
375- value : v . toString ( ) ,
376- } ) ) ,
377- {
378- text : "for the customer's lifetime" ,
379- value : "Infinity" ,
380- } ,
381- ] }
382- />
383- </ InlineBadgePopover >
384- </ >
385- ) }
386- </ span >
387- </ >
354+ < AmountInput />
355+ </ InlineBadgePopover > { " " }
356+ per { selectedEvent }
357+ { selectedEvent === "sale" && (
358+ < >
359+ { " " }
360+ < InlineBadgePopover
361+ text = {
362+ maxDuration === 0
363+ ? "one time"
364+ : maxDuration === Infinity
365+ ? "for the customer's lifetime"
366+ : `for ${ maxDuration } ${ pluralize ( "month" , Number ( maxDuration ) ) } `
367+ }
368+ >
369+ < InlineBadgePopoverMenu
370+ selectedValue = { maxDuration ?. toString ( ) }
371+ onSelect = { ( value ) =>
372+ setValue ( "maxDuration" , Number ( value ) , {
373+ shouldDirty : true ,
374+ } )
375+ }
376+ items = { [
377+ {
378+ text : "one time" ,
379+ value : "0" ,
380+ } ,
381+ ...RECURRING_MAX_DURATIONS . filter (
382+ ( v ) => v !== 0 && v !== 1 , // filter out one-time and 1-month intervals (we only use 1-month for discounts)
383+ ) . map ( ( v ) => ( {
384+ text : `for ${ v } ${ pluralize ( "month" , Number ( v ) ) } ` ,
385+ value : v . toString ( ) ,
386+ } ) ) ,
387+ {
388+ text : "for the customer's lifetime" ,
389+ value : "Infinity" ,
390+ } ,
391+ ] }
392+ />
393+ </ InlineBadgePopover >
394+ </ >
395+ ) }
396+ </ span >
397+ </ div >
398+ < Tooltip
399+ content = { "Add a custom reward description" }
400+ disabled = { description !== null }
401+ >
402+ < div className = "shrink-0" >
403+ < Button
404+ variant = "secondary"
405+ className = { cn (
406+ "size-7 p-0" ,
407+ description !== null && "text-blue-600" ,
408+ ) }
409+ icon = { < Pen2 className = "size-3.5" /> }
410+ onClick = { ( ) =>
411+ setValue (
412+ "description" ,
413+ description === null ? "" : null ,
414+ { shouldDirty : true } ,
415+ )
416+ }
417+ />
418+ </ div >
419+ </ Tooltip >
420+ </ div >
421+ < motion . div
422+ initial = { false }
423+ transition = { { ease : "easeInOut" , duration : 0.2 } }
424+ animate = { {
425+ height : description !== null ? "auto" : 0 ,
426+ opacity : description !== null ? 1 : 0 ,
427+ } }
428+ className = "-mx-2.5 overflow-hidden"
429+ >
430+ < div className = "pt-2.5" >
431+ < div className = "border-border-subtle flex min-w-0 items-center gap-2.5 border-t px-2.5 pt-2.5" >
432+ < RewardIconSquare icon = { Gift } />
433+ < span className = "grow leading-relaxed" >
434+ Shown as{ " " }
435+ < InlineBadgePopover
436+ text = { description || "Reward description" }
437+ invalid = { ! description }
438+ >
439+ < InlineBadgePopoverInput
440+ value = { description ?? "" }
441+ onChange = { ( e ) =>
442+ setValue (
443+ "description" ,
444+ ( e . target as HTMLInputElement ) . value ,
445+ {
446+ shouldDirty : true ,
447+ } ,
448+ )
449+ }
450+ className = "sm:w-80"
451+ maxLength = { 100 }
452+ />
453+ </ InlineBadgePopover >
454+ </ span >
455+ < Button
456+ variant = "outline"
457+ className = "size-6 shrink-0 p-0"
458+ icon = { < X className = "size-3" strokeWidth = { 2 } /> }
459+ onClick = { ( ) =>
460+ setValue ( "description" , null , { shouldDirty : true } )
461+ }
462+ />
463+ </ div >
464+ </ div >
465+ </ motion . div >
466+ </ div >
388467 }
389468 content = {
390469 selectedEvent === "click" ? undefined : (
0 commit comments