feat: add increase generation limit options (#5388)
* wip: limit increase options * feat: add increase AI limit options * fix: overflow issue * UI Updates * UI for bypassing limits * Refactor bypass limit --------- Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>pull/5413/head
parent
e6bea59ab5
commit
18deef46db
12 changed files with 577 additions and 150 deletions
@ -0,0 +1,5 @@ |
|||||||
|
{ |
||||||
|
"devToolbar": { |
||||||
|
"enabled": false |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,68 @@ |
|||||||
|
import { useState } from 'react'; |
||||||
|
import { cn } from '../../lib/classname'; |
||||||
|
import { ChevronUp } from 'lucide-react'; |
||||||
|
import { Modal } from '../Modal'; |
||||||
|
import { ReferYourFriend } from './ReferYourFriend'; |
||||||
|
import { OpenAISettings } from './OpenAISettings'; |
||||||
|
import { PayToBypass } from './PayToBypass'; |
||||||
|
import { PickLimitOption } from './PickLimitOption'; |
||||||
|
import { getOpenAIKey } from '../../lib/jwt.ts'; |
||||||
|
|
||||||
|
export type IncreaseTab = 'api-key' | 'refer-friends' | 'payment'; |
||||||
|
|
||||||
|
export const increaseLimitTabs: { |
||||||
|
key: IncreaseTab; |
||||||
|
title: string; |
||||||
|
}[] = [ |
||||||
|
{ key: 'api-key', title: 'Add your own API Key' }, |
||||||
|
{ key: 'refer-friends', title: 'Refer your Friends' }, |
||||||
|
{ key: 'payment', title: 'Pay to Bypass the limit' }, |
||||||
|
]; |
||||||
|
|
||||||
|
type IncreaseRoadmapLimitProps = { |
||||||
|
onClose: () => void; |
||||||
|
}; |
||||||
|
export function IncreaseRoadmapLimit(props: IncreaseRoadmapLimitProps) { |
||||||
|
const { onClose } = props; |
||||||
|
|
||||||
|
const openAPIKey = getOpenAIKey(); |
||||||
|
const [activeTab, setActiveTab] = useState<IncreaseTab | null>( |
||||||
|
openAPIKey ? 'api-key' : null, |
||||||
|
); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Modal |
||||||
|
onClose={onClose} |
||||||
|
overlayClassName={cn( |
||||||
|
'overscroll-contain', |
||||||
|
activeTab === 'payment' && 'block', |
||||||
|
)} |
||||||
|
wrapperClassName="max-w-lg mx-auto" |
||||||
|
bodyClassName={cn('h-auto pt-px', !activeTab && 'overflow-hidden')} |
||||||
|
> |
||||||
|
{!activeTab && ( |
||||||
|
<PickLimitOption activeTab={activeTab} setActiveTab={setActiveTab} /> |
||||||
|
)} |
||||||
|
|
||||||
|
{activeTab === 'api-key' && ( |
||||||
|
<OpenAISettings |
||||||
|
onClose={() => { |
||||||
|
onClose(); |
||||||
|
}} |
||||||
|
onBack={() => setActiveTab(null)} |
||||||
|
/> |
||||||
|
)} |
||||||
|
{activeTab === 'refer-friends' && ( |
||||||
|
<ReferYourFriend onBack={() => setActiveTab(null)} /> |
||||||
|
)} |
||||||
|
{activeTab === 'payment' && ( |
||||||
|
<PayToBypass |
||||||
|
onBack={() => setActiveTab(null)} |
||||||
|
onClose={() => { |
||||||
|
onClose(); |
||||||
|
}} |
||||||
|
/> |
||||||
|
)} |
||||||
|
</Modal> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,164 @@ |
|||||||
|
import { ChevronLeft } from 'lucide-react'; |
||||||
|
import { useAuth } from '../../hooks/use-auth'; |
||||||
|
|
||||||
|
type PayToBypassProps = { |
||||||
|
onBack: () => void; |
||||||
|
onClose: () => void; |
||||||
|
}; |
||||||
|
|
||||||
|
export function PayToBypass(props: PayToBypassProps) { |
||||||
|
const { onBack, onClose } = props; |
||||||
|
const user = useAuth(); |
||||||
|
|
||||||
|
const userId = 'entry.1665642993'; |
||||||
|
const nameId = 'entry.527005328'; |
||||||
|
const emailId = 'entry.982906376'; |
||||||
|
const amountId = 'entry.1826002937'; |
||||||
|
const roadmapCountId = 'entry.1161404075'; |
||||||
|
const usageId = 'entry.535914744'; |
||||||
|
const feedbackId = 'entry.1024388959'; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="p-4"> |
||||||
|
<button |
||||||
|
onClick={onBack} |
||||||
|
className="mb-5 flex items-center gap-1.5 text-sm leading-none opacity-40 transition-opacity hover:opacity-100 focus:outline-none" |
||||||
|
> |
||||||
|
<ChevronLeft size={16} /> |
||||||
|
Back to options |
||||||
|
</button> |
||||||
|
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800">Pay to Bypass</h2> |
||||||
|
<p className="mt-2 text-sm leading-normal text-gray-500"> |
||||||
|
Tell us more about how you will be using this. |
||||||
|
</p> |
||||||
|
|
||||||
|
<form |
||||||
|
className="mt-4 flex flex-col gap-3" |
||||||
|
action="https://docs.google.com/forms/u/0/d/e/1FAIpQLSeec1oboTc9vCWHxmoKsC5NIbACpQEk7erp8wBKJMz-nzC7LQ/formResponse" |
||||||
|
target="_blank" |
||||||
|
> |
||||||
|
<div className="sr-only" aria-hidden="true"> |
||||||
|
<label htmlFor={userId} className="sr-only"> |
||||||
|
User Id |
||||||
|
</label> |
||||||
|
<input |
||||||
|
id={userId} |
||||||
|
name={userId} |
||||||
|
type="text" |
||||||
|
className="block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
||||||
|
value={user?.id} |
||||||
|
readOnly |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<div className="sr-only" aria-hidden="true"> |
||||||
|
<label htmlFor={nameId} className="sr-only"> |
||||||
|
Name |
||||||
|
</label> |
||||||
|
<input |
||||||
|
id={nameId} |
||||||
|
name={nameId} |
||||||
|
type="text" |
||||||
|
className="block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
||||||
|
value={user?.name} |
||||||
|
readOnly |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<div className="sr-only" aria-hidden="true"> |
||||||
|
<label htmlFor={emailId} className="sr-only"> |
||||||
|
Email |
||||||
|
</label> |
||||||
|
<input |
||||||
|
id={emailId} |
||||||
|
name={emailId} |
||||||
|
type="email" |
||||||
|
className="block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
||||||
|
value={user?.email} |
||||||
|
readOnly |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div> |
||||||
|
<label |
||||||
|
htmlFor={amountId} |
||||||
|
className="mb-2 block text-sm font-semibold" |
||||||
|
> |
||||||
|
How much are you willing to pay for this? |
||||||
|
</label> |
||||||
|
<input |
||||||
|
id={amountId} |
||||||
|
name={amountId} |
||||||
|
type="text" |
||||||
|
required |
||||||
|
className="block w-full rounded-lg border p-3 py-2 shadow-sm outline-none placeholder:text-sm placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
||||||
|
placeholder="How much are you willing to pay for this?" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label |
||||||
|
htmlFor={roadmapCountId} |
||||||
|
className="mb-2 block text-sm font-semibold" |
||||||
|
> |
||||||
|
How many roadmaps you will be generating (daily, or monthly)? |
||||||
|
</label> |
||||||
|
<textarea |
||||||
|
id={roadmapCountId} |
||||||
|
name={roadmapCountId} |
||||||
|
required |
||||||
|
className="placeholder-text-gray-400 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-sm focus:ring-2 focus:ring-black focus:ring-offset-1" |
||||||
|
placeholder="How many roadmaps you will be generating (daily, or monthly)?" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label htmlFor={usageId} className="mb-2 block text-sm font-semibold"> |
||||||
|
How will you be using this? |
||||||
|
</label> |
||||||
|
<textarea |
||||||
|
id={usageId} |
||||||
|
name={usageId} |
||||||
|
required |
||||||
|
className="placeholder-text-gray-400 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-sm focus:ring-2 focus:ring-black focus:ring-offset-1" |
||||||
|
placeholder="How will you be using this" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label |
||||||
|
htmlFor={feedbackId} |
||||||
|
className="mb-2 block text-sm font-semibold" |
||||||
|
> |
||||||
|
Do you have any feedback? |
||||||
|
</label> |
||||||
|
<textarea |
||||||
|
id={feedbackId} |
||||||
|
name={feedbackId} |
||||||
|
className="placeholder-text-gray-400 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-sm focus:ring-2 focus:ring-black focus:ring-offset-1" |
||||||
|
placeholder="Do you have any feedback?" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2"> |
||||||
|
<button |
||||||
|
type="button" |
||||||
|
className="disbaled:opacity-60 w-full rounded-lg border border-gray-300 py-2 text-sm hover:bg-gray-100 disabled:cursor-not-allowed" |
||||||
|
onClick={() => { |
||||||
|
onClose(); |
||||||
|
}} |
||||||
|
> |
||||||
|
Cancel |
||||||
|
</button> |
||||||
|
<button |
||||||
|
type="submit" |
||||||
|
className="disbaled:opacity-60 w-full rounded-lg bg-gray-900 py-2 text-sm text-white hover:bg-gray-800 disabled:cursor-not-allowed" |
||||||
|
onClick={() => { |
||||||
|
setTimeout(() => { |
||||||
|
onClose(); |
||||||
|
}, 100); |
||||||
|
}} |
||||||
|
> |
||||||
|
Submit |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
import { ChevronRight, ChevronUpIcon } from 'lucide-react'; |
||||||
|
import { cn } from '../../lib/classname'; |
||||||
|
import { increaseLimitTabs, type IncreaseTab } from './IncreaseRoadmapLimit'; |
||||||
|
|
||||||
|
type PickLimitOptionProps = { |
||||||
|
activeTab: IncreaseTab | null; |
||||||
|
setActiveTab: (tab: IncreaseTab | null) => void; |
||||||
|
}; |
||||||
|
|
||||||
|
export function PickLimitOption(props: PickLimitOptionProps) { |
||||||
|
const { activeTab, setActiveTab } = props; |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<div className="p-4"> |
||||||
|
<h2 className="text-xl font-semibold text-gray-800"> |
||||||
|
Generate more Roadmaps |
||||||
|
</h2> |
||||||
|
<p className="mt-2 text-sm text-gray-700"> |
||||||
|
Pick one of the options below to increase your roadmap limit. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="flex w-full flex-col gap-1 px-3 pb-4"> |
||||||
|
{increaseLimitTabs.map((tab) => { |
||||||
|
const isActive = tab.key === activeTab; |
||||||
|
|
||||||
|
return ( |
||||||
|
<button |
||||||
|
key={tab.key} |
||||||
|
onClick={() => { |
||||||
|
setActiveTab(isActive ? null : tab.key); |
||||||
|
}} |
||||||
|
className={cn( |
||||||
|
'flex w-full items-center justify-between gap-2 rounded-md border-t py-2 text-sm font-medium pl-3 pr-3', |
||||||
|
{ |
||||||
|
'bg-gray-100 text-gray-800': isActive, |
||||||
|
'bg-gray-200 hover:bg-gray-300 transition-colors text-black': !isActive, |
||||||
|
}, |
||||||
|
)} |
||||||
|
> |
||||||
|
{tab.title} |
||||||
|
<ChevronRight size={16} /> |
||||||
|
</button> |
||||||
|
); |
||||||
|
})} |
||||||
|
</div> |
||||||
|
</> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,84 @@ |
|||||||
|
import { Check, ChevronLeft, Clipboard } from 'lucide-react'; |
||||||
|
import { useAuth } from '../../hooks/use-auth'; |
||||||
|
import { useCopyText } from '../../hooks/use-copy-text'; |
||||||
|
import { useToast } from '../../hooks/use-toast'; |
||||||
|
import { useRef } from 'react'; |
||||||
|
import { cn } from '../../lib/classname.ts'; |
||||||
|
|
||||||
|
type ReferYourFriendProps = { |
||||||
|
onBack: () => void; |
||||||
|
}; |
||||||
|
|
||||||
|
export function ReferYourFriend(props: ReferYourFriendProps) { |
||||||
|
const { onBack } = props; |
||||||
|
|
||||||
|
const user = useAuth(); |
||||||
|
const toast = useToast(); |
||||||
|
const inputRef = useRef<HTMLInputElement>(null); |
||||||
|
|
||||||
|
const { copyText, isCopied } = useCopyText(); |
||||||
|
const referralLink = new URL( |
||||||
|
`/ai?rc=${user?.id}`, |
||||||
|
import.meta.env.DEV ? 'http://localhost:3000' : 'https://roadmap.sh', |
||||||
|
).toString(); |
||||||
|
|
||||||
|
const handleCopy = () => { |
||||||
|
inputRef.current?.select(); |
||||||
|
copyText(referralLink); |
||||||
|
toast.success('Copied to clipboard'); |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="p-4"> |
||||||
|
<button |
||||||
|
onClick={onBack} |
||||||
|
className="mb-5 flex items-center gap-1.5 text-sm leading-none opacity-40 transition-opacity hover:opacity-100 focus:outline-none" |
||||||
|
> |
||||||
|
<ChevronLeft size={16} /> |
||||||
|
Back to options |
||||||
|
</button> |
||||||
|
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800"> |
||||||
|
Refer your Friends |
||||||
|
</h2> |
||||||
|
<p className="mt-2 text-sm text-gray-500"> |
||||||
|
Share the URL below with your friends. When they sign up with your link, |
||||||
|
you will get extra roadmap generation credits. |
||||||
|
</p> |
||||||
|
|
||||||
|
<label className="mt-4 flex flex-col gap-2"> |
||||||
|
<input |
||||||
|
ref={inputRef} |
||||||
|
className="w-full rounded-md border bg-gray-100 p-2 px-2.5 text-gray-700 focus:outline-none" |
||||||
|
value={referralLink} |
||||||
|
readOnly={true} |
||||||
|
onClick={handleCopy} |
||||||
|
/> |
||||||
|
|
||||||
|
<button |
||||||
|
className={cn( |
||||||
|
'flex h-10 items-center justify-center gap-1.5 rounded-md p-2 px-2.5 text-sm', |
||||||
|
{ |
||||||
|
'bg-green-500 text-black transition-colors': isCopied, |
||||||
|
'bg-black text-white rounded-md': !isCopied, |
||||||
|
}, |
||||||
|
)} |
||||||
|
onClick={handleCopy} |
||||||
|
disabled={isCopied} |
||||||
|
> |
||||||
|
{isCopied ? ( |
||||||
|
<> |
||||||
|
<Check className="h-4 w-4" /> |
||||||
|
Copied to Clipboard |
||||||
|
</> |
||||||
|
) : ( |
||||||
|
<> |
||||||
|
<Clipboard className="h-4 w-4" /> |
||||||
|
Copy URL |
||||||
|
</> |
||||||
|
)} |
||||||
|
</button> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
Loading…
Reference in new issue