mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-13 20:54:16 +02:00
Block UI for guest users
This commit is contained in:
@@ -35,6 +35,7 @@ import { cn } from '../../lib/classname.ts';
|
||||
import { RoadmapTopicDetail } from './RoadmapTopicDetail.tsx';
|
||||
import { AIRoadmapAlert } from './AIRoadmapAlert.tsx';
|
||||
import { OpenAISettings } from './OpenAISettings.tsx';
|
||||
import { IS_KEY_ONLY_ROADMAP_GENERATION } from '../../lib/ai.ts';
|
||||
|
||||
export type GetAIRoadmapLimitResponse = {
|
||||
used: number;
|
||||
@@ -104,6 +105,7 @@ export function GenerateRoadmap() {
|
||||
const [isConfiguring, setIsConfiguring] = useState(false);
|
||||
|
||||
const openAPIKey = getOpenAIKey();
|
||||
const isKeyOnly = IS_KEY_ONLY_ROADMAP_GENERATION;
|
||||
|
||||
const renderRoadmap = async (roadmap: string) => {
|
||||
const { nodes, edges } = generateAIRoadmapFromText(roadmap);
|
||||
@@ -372,6 +374,7 @@ export function GenerateRoadmap() {
|
||||
limit={roadmapLimit}
|
||||
limitUsed={roadmapLimitUsed}
|
||||
loadAIRoadmapLimit={loadAIRoadmapLimit}
|
||||
isKeyOnly={isKeyOnly}
|
||||
onLoadTerm={(term: string) => {
|
||||
setRoadmapTerm(term);
|
||||
loadTermRoadmap(term).finally(() => {});
|
||||
@@ -427,63 +430,93 @@ export function GenerateRoadmap() {
|
||||
</span>
|
||||
)}
|
||||
{!isLoading && (
|
||||
<div className="container flex flex-grow flex-col items-center">
|
||||
<div className="container flex flex-grow flex-col items-start">
|
||||
<AIRoadmapAlert />
|
||||
<div className="mt-2 flex w-full flex-col items-start justify-between gap-2 text-sm sm:flex-row sm:items-center sm:gap-0">
|
||||
<span>
|
||||
<span
|
||||
className={cn(
|
||||
'mr-0.5 inline-block rounded-xl border px-1.5 text-center text-sm tabular-nums text-gray-800',
|
||||
{
|
||||
'animate-pulse border-zinc-300 bg-zinc-300 text-zinc-300':
|
||||
!roadmapLimit,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{roadmapLimitUsed} of {roadmapLimit}
|
||||
</span>{' '}
|
||||
roadmaps generated.
|
||||
</span>
|
||||
{!isLoggedInUser && (
|
||||
<button
|
||||
className="rounded-xl border border-current px-1.5 py-0.5 text-left text-sm font-medium text-blue-500 sm:text-center"
|
||||
onClick={showLoginPopup}
|
||||
>
|
||||
Generate more by{' '}
|
||||
<span className="font-semibold">
|
||||
signing up (free, takes 2s)
|
||||
{isKeyOnly && (
|
||||
<div className="flex flex-row gap-4">
|
||||
{!openAPIKey && (
|
||||
<p className={'text-left text-red-500'}>
|
||||
We have hit the limit for AI roadmap generation. Please
|
||||
try again tomorrow or{' '}
|
||||
<button
|
||||
onClick={() => setIsConfiguring(true)}
|
||||
className="font-semibold text-purple-600 underline underline-offset-2"
|
||||
>
|
||||
add your own OpenAI API key
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
{openAPIKey && (
|
||||
<p className={'text-left text-gray-500'}>
|
||||
You have added your own OpenAI API key.{' '}
|
||||
<button
|
||||
onClick={() => setIsConfiguring(true)}
|
||||
className="font-semibold text-purple-600 underline underline-offset-2"
|
||||
>
|
||||
Configure it here if you want.
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isKeyOnly && (
|
||||
<div className="mt-2 flex w-full flex-col items-start justify-between gap-2 text-sm sm:flex-row sm:items-center sm:gap-0">
|
||||
<span>
|
||||
<span
|
||||
className={cn(
|
||||
'mr-0.5 inline-block rounded-xl border px-1.5 text-center text-sm tabular-nums text-gray-800',
|
||||
{
|
||||
'animate-pulse border-zinc-300 bg-zinc-300 text-zinc-300':
|
||||
!roadmapLimit,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{roadmapLimitUsed} of {roadmapLimit}
|
||||
</span>{' '}
|
||||
or <span className="font-semibold">logging in</span>
|
||||
</button>
|
||||
)}
|
||||
{isLoggedInUser && !openAPIKey && (
|
||||
<button
|
||||
onClick={() => setIsConfiguring(true)}
|
||||
className="text-left rounded-xl border border-current px-2 py-0.5 text-sm text-blue-500 transition-colors hover:bg-blue-400 hover:text-white"
|
||||
>
|
||||
By-pass all limits by{' '}
|
||||
<span className="font-semibold">
|
||||
adding your own OpenAI API key
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
roadmaps generated.
|
||||
</span>
|
||||
{!isLoggedInUser && (
|
||||
<button
|
||||
className="rounded-xl border border-current px-1.5 py-0.5 text-left text-sm font-medium text-blue-500 sm:text-center"
|
||||
onClick={showLoginPopup}
|
||||
>
|
||||
Generate more by{' '}
|
||||
<span className="font-semibold">
|
||||
signing up (free, takes 2s)
|
||||
</span>{' '}
|
||||
or <span className="font-semibold">logging in</span>
|
||||
</button>
|
||||
)}
|
||||
{isLoggedInUser && !openAPIKey && (
|
||||
<button
|
||||
onClick={() => setIsConfiguring(true)}
|
||||
className="rounded-xl border border-current px-2 py-0.5 text-left text-sm text-blue-500 transition-colors hover:bg-blue-400 hover:text-white"
|
||||
>
|
||||
By-pass all limits by{' '}
|
||||
<span className="font-semibold">
|
||||
adding your own OpenAI API key
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isLoggedInUser && openAPIKey && (
|
||||
<button
|
||||
onClick={() => setIsConfiguring(true)}
|
||||
className="flex flex-row items-center gap-1 rounded-xl border border-current px-2 py-0.5 text-sm text-blue-500 transition-colors hover:bg-blue-400 hover:text-white"
|
||||
>
|
||||
<Cog size={15} />
|
||||
Configure OpenAI key
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{isLoggedInUser && openAPIKey && (
|
||||
<button
|
||||
onClick={() => setIsConfiguring(true)}
|
||||
className="flex flex-row items-center gap-1 rounded-xl border border-current px-2 py-0.5 text-sm text-blue-500 transition-colors hover:bg-blue-400 hover:text-white"
|
||||
>
|
||||
<Cog size={15} />
|
||||
Configure OpenAI key
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="my-3 flex w-full flex-col gap-2 sm:flex-row sm:items-center sm:justify-center"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
placeholder="e.g. Try searching for Ansible or DevOps"
|
||||
className="flex-grow rounded-md border border-gray-400 px-3 py-2 transition-colors focus:border-black focus:outline-none"
|
||||
value={roadmapTerm}
|
||||
@@ -501,7 +534,8 @@ export function GenerateRoadmap() {
|
||||
!roadmapLimit ||
|
||||
!roadmapTerm ||
|
||||
roadmapLimitUsed >= roadmapLimit ||
|
||||
roadmapTerm === currentRoadmap?.term
|
||||
roadmapTerm === currentRoadmap?.term ||
|
||||
(isKeyOnly && !openAPIKey)
|
||||
}
|
||||
>
|
||||
{roadmapLimit > 0 && canGenerateMore && (
|
||||
@@ -514,7 +548,7 @@ export function GenerateRoadmap() {
|
||||
{roadmapLimit === 0 && <span>Please wait..</span>}
|
||||
|
||||
{roadmapLimit > 0 && !canGenerateMore && (
|
||||
<span className="flex items-center text-sm">
|
||||
<span className="flex items-center">
|
||||
<Ban size={15} className="mr-2" />
|
||||
Limit reached
|
||||
</span>
|
||||
|
@@ -1,10 +1,6 @@
|
||||
import { Modal } from '../Modal.tsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
deleteOpenAIKey,
|
||||
getOpenAIKey,
|
||||
saveOpenAIKey,
|
||||
} from '../../lib/jwt.ts';
|
||||
import { deleteOpenAIKey, getOpenAIKey, saveOpenAIKey } from '../../lib/jwt.ts';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import { CloseIcon } from '../ReactIcons/CloseIcon.tsx';
|
||||
import { useToast } from '../../hooks/use-toast.ts';
|
||||
@@ -121,6 +117,10 @@ export function OpenAISettings(props: OpenAISettingsProps) {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className={'mb-2 mt-1 text-xs text-gray-500'}>
|
||||
We do not store your API key on our servers.
|
||||
</p>
|
||||
|
||||
{hasError && (
|
||||
<p className="mt-2 text-sm text-red-500">
|
||||
Please enter a valid OpenAI API key
|
||||
|
@@ -21,6 +21,7 @@ type RoadmapSearchProps = {
|
||||
onLoadTerm: (topic: string) => void;
|
||||
limit: number;
|
||||
limitUsed: number;
|
||||
isKeyOnly: boolean;
|
||||
};
|
||||
|
||||
export function RoadmapSearch(props: RoadmapSearchProps) {
|
||||
@@ -32,16 +33,18 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
|
||||
limitUsed = 0,
|
||||
onLoadTerm,
|
||||
loadAIRoadmapLimit,
|
||||
isKeyOnly,
|
||||
} = props;
|
||||
|
||||
const canGenerateMore = limitUsed < limit;
|
||||
const [isConfiguring, setIsConfiguring] = useState(false);
|
||||
const openAPIKey = getOpenAIKey();
|
||||
const isAuthenticatedUser = isLoggedIn();
|
||||
|
||||
const randomTerms = ['OAuth', 'APIs', 'UX Design', 'gRPC'];
|
||||
|
||||
return (
|
||||
<div className="flex flex-grow flex-col items-center sm:justify-center px-4 py-6 sm:px-6">
|
||||
<div className="flex flex-grow flex-col items-center px-4 py-6 sm:px-6">
|
||||
{isConfiguring && (
|
||||
<OpenAISettings
|
||||
onClose={() => {
|
||||
@@ -50,7 +53,7 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col gap-0 text-center sm:gap-2">
|
||||
<div className="flex flex-col gap-0 text-center sm:gap-2 md:mt-24 lg:mt-32">
|
||||
<h1 className="relative text-2xl font-medium sm:text-3xl">
|
||||
<span className="hidden sm:inline">Generate roadmaps with AI</span>
|
||||
<span className="inline sm:hidden">AI Roadmap Generator</span>
|
||||
@@ -90,30 +93,57 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
|
||||
'flex min-w-[154px] flex-shrink-0 items-center justify-center gap-2 rounded-md bg-black px-4 py-2 text-white',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
)}
|
||||
disabled={!limit || !roadmapTerm || limitUsed >= limit}
|
||||
onClick={(e) => {
|
||||
if (!isAuthenticatedUser) {
|
||||
e.preventDefault();
|
||||
showLoginPopup();
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
isAuthenticatedUser &&
|
||||
(!limit ||
|
||||
!roadmapTerm ||
|
||||
limitUsed >= limit ||
|
||||
(isKeyOnly && !openAPIKey))
|
||||
}
|
||||
>
|
||||
{(!limit || canGenerateMore) && (
|
||||
{!isAuthenticatedUser && (
|
||||
<>
|
||||
<Wand size={20} />
|
||||
Generate
|
||||
</>
|
||||
)}
|
||||
{isAuthenticatedUser && (
|
||||
<>
|
||||
{(!limit || canGenerateMore) && (
|
||||
<>
|
||||
<Wand size={20} />
|
||||
Generate
|
||||
</>
|
||||
)}
|
||||
|
||||
{limit > 0 && !canGenerateMore && (
|
||||
<span className="flex items-center text-base">
|
||||
<Ban size={15} className="mr-2" />
|
||||
Limit reached
|
||||
</span>
|
||||
{limit > 0 && !canGenerateMore && (
|
||||
<span className="flex items-center text-base">
|
||||
<Ban size={15} className="mr-2" />
|
||||
Limit reached
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
<div className="flex flex-row items-center justify-center gap-2 flex-wrap">
|
||||
<div className="flex flex-row flex-wrap items-center justify-center gap-2">
|
||||
{randomTerms.map((term) => (
|
||||
<button
|
||||
key={term}
|
||||
disabled={!limit || !canGenerateMore}
|
||||
disabled={isAuthenticatedUser && (!limit || !canGenerateMore)}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!isAuthenticatedUser) {
|
||||
showLoginPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
onLoadTerm(term);
|
||||
}}
|
||||
className="flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-sm transition-colors hover:border-black hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
@@ -129,60 +159,118 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-12 flex flex-col items-center gap-4">
|
||||
<p className="text-gray-500 text-center">
|
||||
You have generated{' '}
|
||||
<span
|
||||
className={cn(
|
||||
'inline-block min-w-[50px] rounded-xl border px-1.5 text-center text-sm tabular-nums text-gray-800',
|
||||
{
|
||||
'animate-pulse border-zinc-300 bg-zinc-300 text-zinc-300':
|
||||
!limit,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{limitUsed} of {limit}
|
||||
</span>{' '}
|
||||
roadmaps.
|
||||
</p>
|
||||
<p className="flex min-h-[46px] sm:min-h-[26px] items-center text-sm">
|
||||
{limit > 0 && !isLoggedIn() && (
|
||||
{!isAuthenticatedUser && (
|
||||
<div className="mt-8 flex max-w-[500px] flex-col items-center gap-3 rounded-xl border border-gray-400 px-4 pb-4 pt-3">
|
||||
<p className={'text-center text-gray-500'}>
|
||||
<button
|
||||
onClick={showLoginPopup}
|
||||
className="rounded-xl border border-current px-2 py-0.5 text-sm text-blue-500 transition-colors hover:bg-blue-400 hover:text-white"
|
||||
className="font-medium text-purple-600 underline underline-offset-2 hover:text-purple-800"
|
||||
>
|
||||
Generate more by{' '}
|
||||
<span className="font-semibold">
|
||||
signing up (free and takes 2 seconds)
|
||||
</span>{' '}
|
||||
or <span className="font-semibold">logging in</span>
|
||||
</button>
|
||||
Sign up (free and takes 2s) or login
|
||||
</button>{' '}
|
||||
to start generating roadmaps. Or explore the ones made by the
|
||||
community.
|
||||
</p>
|
||||
<p className="flex flex-col gap-2 text-center text-gray-500 sm:flex-row">
|
||||
<a
|
||||
href="/ai/explore"
|
||||
className="flex items-center gap-1.5 rounded-full border border-purple-600 px-2.5 py-0.5 text-sm text-purple-600 transition-colors hover:bg-purple-600 hover:text-white"
|
||||
>
|
||||
Explore AI Generated Roadmaps <Telescope size={15} />
|
||||
</a>
|
||||
<a
|
||||
href="/roadmaps"
|
||||
className="flex items-center gap-1.5 rounded-full border border-purple-600 px-2.5 py-0.5 text-sm text-purple-600 transition-colors hover:bg-purple-600 hover:text-white"
|
||||
>
|
||||
Visit Official Roadmaps <ArrowUpRight size={15} />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{isKeyOnly && isAuthenticatedUser && (
|
||||
<div className="mx-auto mt-12 flex max-w-[450px] flex-col items-center gap-4">
|
||||
{!openAPIKey && (
|
||||
<>
|
||||
<p className={'text-center text-red-500'}>
|
||||
We have hit the limit for AI roadmap generation. Please try
|
||||
again later or{' '}
|
||||
<button
|
||||
onClick={() => setIsConfiguring(true)}
|
||||
className="font-semibold text-purple-600 underline underline-offset-2"
|
||||
>
|
||||
add your own OpenAI API key.
|
||||
</button>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<p className="-mt-[75px] sm:-mt-[46px] flex min-h-[46px] sm:min-h-[26px] items-center text-sm">
|
||||
{limit > 0 && isLoggedIn() && !openAPIKey && (
|
||||
<button
|
||||
onClick={() => setIsConfiguring(true)}
|
||||
className="rounded-xl border border-current px-2 py-0.5 text-sm text-blue-500 transition-colors hover:bg-blue-400 hover:text-white"
|
||||
>
|
||||
By-pass all limits by{' '}
|
||||
<span className="font-semibold">
|
||||
adding your own OpenAI API key
|
||||
</span>
|
||||
</button>
|
||||
{openAPIKey && (
|
||||
<p className={'text-center text-gray-500'}>
|
||||
You have added your own OpenAI API key.{' '}
|
||||
<button
|
||||
onClick={() => setIsConfiguring(true)}
|
||||
className="font-semibold text-purple-600 underline underline-offset-2"
|
||||
>
|
||||
Configure it here if you want.
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{limit > 0 && isLoggedIn() && openAPIKey && (
|
||||
<button
|
||||
onClick={() => setIsConfiguring(true)}
|
||||
className="flex flex-row items-center gap-1 rounded-xl border border-current px-2 py-0.5 text-sm text-blue-500 transition-colors hover:bg-blue-400 hover:text-white"
|
||||
<p className="flex flex-col gap-2 text-center text-gray-500 sm:flex-row">
|
||||
<a
|
||||
href="/ai/explore"
|
||||
className="flex items-center gap-1.5 rounded-full border border-purple-600 px-2.5 py-0.5 text-sm text-purple-600 transition-colors hover:bg-purple-600 hover:text-white"
|
||||
>
|
||||
<Cog size={15} />
|
||||
Configure OpenAI key
|
||||
</button>
|
||||
Explore AI Roadmaps <Telescope size={15} />
|
||||
</a>
|
||||
<a
|
||||
href="/roadmaps"
|
||||
className="flex items-center gap-1.5 rounded-full border border-purple-600 px-2.5 py-0.5 text-sm text-purple-600 transition-colors hover:bg-purple-600 hover:text-white"
|
||||
>
|
||||
Visit Official Roadmaps <ArrowUpRight size={15} />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!isKeyOnly && limit > 0 && isAuthenticatedUser && (
|
||||
<div className="mt-12 flex flex-col items-center gap-4">
|
||||
<p className="text-center text-gray-500">
|
||||
You have generated{' '}
|
||||
<span
|
||||
className={
|
||||
'inline-block min-w-[50px] rounded-xl border px-1.5 text-center text-sm tabular-nums text-gray-800'
|
||||
}
|
||||
>
|
||||
{limitUsed} of {limit}
|
||||
</span>{' '}
|
||||
roadmaps.
|
||||
</p>
|
||||
{isAuthenticatedUser && (
|
||||
<p className="flex items-center text-sm">
|
||||
{!openAPIKey && (
|
||||
<button
|
||||
onClick={() => setIsConfiguring(true)}
|
||||
className="rounded-xl border border-current px-2 py-0.5 text-sm text-blue-500 transition-colors hover:bg-blue-400 hover:text-white"
|
||||
>
|
||||
By-pass all limits by{' '}
|
||||
<span className="font-semibold">
|
||||
adding your own OpenAI API key
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{openAPIKey && (
|
||||
<button
|
||||
onClick={() => setIsConfiguring(true)}
|
||||
className="flex flex-row items-center gap-1 rounded-xl border border-current px-2 py-0.5 text-sm text-blue-500 transition-colors hover:bg-blue-400 hover:text-white"
|
||||
>
|
||||
<Cog size={15} />
|
||||
Configure OpenAI key
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
1
src/lib/ai.ts
Normal file
1
src/lib/ai.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const IS_KEY_ONLY_ROADMAP_GENERATION = false;
|
Reference in New Issue
Block a user