Skip to content

Commit 147e0ad

Browse files
committed
Replace /referral with /refer-friends and let them copy referral url
1 parent cbad21d commit 147e0ad

File tree

5 files changed

+126
-14
lines changed

5 files changed

+126
-14
lines changed

cli/src/commands/command-registry.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,8 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
230230
},
231231
}),
232232
defineCommandWithArgs({
233-
name: 'referral',
234-
aliases: ['redeem'],
233+
name: 'refer-friends',
234+
aliases: ['referral', 'redeem'],
235235
handler: async (params, args) => {
236236
const trimmedArgs = args.trim()
237237

cli/src/components/bottom-banner.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export interface BottomBannerConfig {
3232
children?: React.ReactNode
3333
/** Called when close button is clicked. If not provided, no close button is shown. */
3434
onClose?: () => void
35+
/** Which border sides to render. Defaults to ['bottom', 'left', 'right']. */
36+
border?: ('top' | 'bottom' | 'left' | 'right')[]
3537
}
3638

3739
export type BottomBannerProps = BottomBannerConfig
@@ -66,6 +68,7 @@ export const BottomBanner: React.FC<BottomBannerProps> = ({
6668
text,
6769
children,
6870
onClose,
71+
border,
6972
}) => {
7073
const { width, terminalWidth } = useTerminalLayout()
7174
const theme = useTheme()
@@ -96,7 +99,7 @@ export const BottomBanner: React.FC<BottomBannerProps> = ({
9699
marginTop: 0,
97100
marginBottom: 0,
98101
}}
99-
border={['bottom', 'left', 'right']}
102+
border={border ?? ['bottom', 'left', 'right']}
100103
customBorderChars={BORDER_CHARS}
101104
>
102105
{hasTextContent ? (

cli/src/components/chat-input-bar.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,11 @@ export const ChatInputBar = ({
195195
return <InputModeBanner />
196196
}
197197

198+
// Referral mode: show only the referral banner (no input box)
199+
if (inputMode === 'referral') {
200+
return <InputModeBanner />
201+
}
202+
198203
// Handle input changes with special mode entry detection
199204
const handleInputChange = (value: InputValue) => {
200205
// Detect entering bash mode: user typed exactly '!' when in default mode
Lines changed: 108 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,122 @@
1+
import { CREDITS_REFERRAL_BONUS } from '@codebuff/common/old-constants'
12
import { WEBSITE_URL } from '@codebuff/sdk'
2-
import React from 'react'
3+
import { useQuery } from '@tanstack/react-query'
4+
import React, { useState } from 'react'
35

46
import { BottomBanner } from './bottom-banner'
7+
import { Button } from './button'
58
import { useChatStore } from '../state/chat-store'
9+
import { useTheme } from '../hooks/use-theme'
10+
import { useTimeout } from '../hooks/use-timeout'
11+
import { getAuthToken } from '../utils/auth'
12+
import { getApiClient } from '../utils/codebuff-api'
13+
import { copyTextToClipboard } from '../utils/clipboard'
14+
import { BORDER_CHARS } from '../utils/ui-constants'
15+
16+
interface ReferralData {
17+
referralCode: string
18+
referrals: { id: string }[]
19+
referralLimit: number
20+
}
621

722
export const ReferralBanner = () => {
823
const setInputMode = useChatStore((state) => state.setInputMode)
24+
const theme = useTheme()
25+
const [isHovered, setIsHovered] = useState(false)
26+
const [isCopied, setIsCopied] = useState(false)
27+
const { setTimeout } = useTimeout()
28+
const authToken = getAuthToken()
29+
30+
const { data: referralData } = useQuery({
31+
queryKey: ['referrals'],
32+
queryFn: async () => {
33+
const client = getApiClient()
34+
const response = await client.get<ReferralData>('/api/referrals', {
35+
includeCookie: true,
36+
})
37+
if (!response.ok) {
38+
throw new Error(`Failed to fetch referral data: ${response.status}`)
39+
}
40+
return response.data!
41+
},
42+
enabled: !!authToken,
43+
staleTime: 5 * 60 * 1000,
44+
retry: false,
45+
})
46+
47+
const referralCode = referralData?.referralCode ?? null
48+
const referralLink = referralCode ? `${WEBSITE_URL}/referrals/${referralCode}` : null
49+
const referralCount = referralData?.referrals.length ?? null
50+
const referralLimit = referralData?.referralLimit ?? null
951

10-
const referralUrl = `${WEBSITE_URL}/referrals`
52+
const handleCopy = async () => {
53+
if (!referralLink) return
54+
try {
55+
await copyTextToClipboard(referralLink, { suppressGlobalMessage: true })
56+
setIsCopied(true)
57+
setTimeout('reset-copied', () => setIsCopied(false), 2000)
58+
} catch {
59+
// Error is already logged and displayed by copyTextToClipboard
60+
}
61+
}
62+
63+
const copyLabel = isCopied ? '✔ Copied!' : '⎘ Copy referral link'
1164

1265
return (
1366
<BottomBanner
14-
borderColorKey="warning"
15-
text={`Refer your friends: ${referralUrl}`}
67+
borderColorKey="primary"
68+
border={['top', 'bottom', 'left', 'right']}
1669
onClose={() => setInputMode('default')}
17-
/>
70+
>
71+
<box style={{ flexDirection: 'column', gap: 0, flexGrow: 1, marginRight: 3 }}>
72+
<text style={{ fg: theme.foreground }}>
73+
{`Share this link with friends and you'll both earn ${CREDITS_REFERRAL_BONUS} credits`}
74+
</text>
75+
76+
{referralCount !== null && referralLimit !== null && (
77+
<text style={{ fg: theme.muted }}>
78+
{`You've referred ${referralCount}/${referralLimit} people`}
79+
</text>
80+
)}
81+
82+
{referralLink ? (
83+
<box style={{ flexDirection: 'column', gap: 0 }}>
84+
<text style={{ fg: theme.muted }}>{referralLink}</text>
85+
<box style={{ flexDirection: 'row', paddingTop: 0 }}>
86+
<Button
87+
onClick={handleCopy}
88+
onMouseOver={() => setIsHovered(true)}
89+
onMouseOut={() => setIsHovered(false)}
90+
style={{
91+
paddingLeft: 1,
92+
paddingRight: 1,
93+
borderStyle: 'single',
94+
borderColor: isCopied
95+
? 'green'
96+
: isHovered
97+
? theme.foreground
98+
: theme.primary,
99+
customBorderChars: BORDER_CHARS,
100+
}}
101+
>
102+
<text
103+
style={{
104+
fg: isCopied
105+
? 'green'
106+
: isHovered
107+
? theme.foreground
108+
: theme.primary,
109+
}}
110+
>
111+
{copyLabel}
112+
</text>
113+
</Button>
114+
</box>
115+
</box>
116+
) : (
117+
<text style={{ fg: theme.muted }}>Loading referral link...</text>
118+
)}
119+
</box>
120+
</BottomBanner>
18121
)
19122
}

cli/src/data/slash-commands.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AGENT_MODES } from '../utils/constants'
2+
import { CREDITS_REFERRAL_BONUS } from '@codebuff/common/old-constants'
23

34
import type { SkillsMap } from '@codebuff/common/types/skill'
45

@@ -51,6 +52,12 @@ export const SLASH_COMMANDS: SlashCommand[] = [
5152
label: 'ads:disable',
5253
description: 'Disable contextual ads and stop earning credits',
5354
},
55+
{
56+
id: 'refer-friends',
57+
label: 'refer-friends',
58+
description: `Refer friends for ${CREDITS_REFERRAL_BONUS} bonus credits each`,
59+
aliases: ['referral'],
60+
},
5461
{
5562
id: 'init',
5663
label: 'init',
@@ -127,12 +134,6 @@ export const SLASH_COMMANDS: SlashCommand[] = [
127134
aliases: ['img', 'attach'],
128135
},
129136
...MODE_COMMANDS,
130-
{
131-
id: 'referral',
132-
label: 'referral',
133-
description: 'Redeem a referral code for bonus credits',
134-
aliases: ['redeem'],
135-
},
136137
// {
137138
// id: 'publish',
138139
// label: 'publish',

0 commit comments

Comments
 (0)