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