1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-08 16:20:40 +02:00
This commit is contained in:
Arik Chakma
2025-06-13 12:08:35 +06:00
parent 7af779d6ba
commit 16de168751
5 changed files with 1382 additions and 1 deletions

View File

@@ -72,6 +72,7 @@
"npm-check-updates": "^18.0.1",
"playwright": "^1.52.0",
"prismjs": "^1.30.0",
"radix-ui": "^1.4.2",
"react": "^19.1.0",
"react-calendar-heatmap": "^1.10.0",
"react-confetti": "^6.4.0",

1010
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,183 @@
import {
BookOpenIcon,
FileTextIcon,
MapIcon,
SparklesIcon,
type LucideIcon,
} from 'lucide-react';
import { useId, useState } from 'react';
import { cn } from '../../lib/classname';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '../Select';
const allowedFormats = ['course', 'guide', 'roadmap'] as const;
type AllowedFormat = (typeof allowedFormats)[number];
export function ContentGenerator() {
const [title, setTitle] = useState('');
const [selectedFormat, setSelectedFormat] = useState<AllowedFormat>('course');
const titleFieldId = useId();
const allowedFormats: {
label: string;
icon: LucideIcon;
value: AllowedFormat;
}[] = [
{
label: 'Course',
icon: BookOpenIcon,
value: 'course',
},
{
label: 'Guide',
icon: FileTextIcon,
value: 'guide',
},
{
label: 'Roadmap',
icon: MapIcon,
value: 'roadmap',
},
];
return (
<form className="mx-auto mt-20 w-full max-w-md space-y-4 rounded-xl bg-white p-4">
<div className="flex flex-col gap-2">
<label
htmlFor={titleFieldId}
className="inline-block text-sm text-gray-500"
>
What can I help you learn?
</label>
<input
type="text"
id={titleFieldId}
placeholder="Enter a topic"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="block h-9 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm outline-none placeholder:text-gray-500 focus:border-gray-500"
/>
</div>
<div className="flex flex-col gap-2">
<label className="inline-block text-sm text-gray-500">
Choose the format
</label>
<div className="grid grid-cols-3 gap-2">
{allowedFormats.map((format) => {
const isSelected = format.value === selectedFormat;
return (
<FormatItem
key={format.value}
label={format.label}
onClick={() => setSelectedFormat(format.value)}
icon={format.icon}
isSelected={isSelected}
/>
);
})}
</div>
</div>
<GuideOptions />
<button
type="submit"
className="flex h-9 w-full items-center justify-center gap-2 rounded-lg bg-black text-sm text-white focus:outline-none"
>
<SparklesIcon className="size-4" />
Generate
</button>
</form>
);
}
type FormatItemProps = {
label: string;
onClick: () => void;
icon: LucideIcon;
isSelected: boolean;
};
function FormatItem(props: FormatItemProps) {
const { label, onClick, icon: Icon, isSelected } = props;
return (
<button
type="button"
className={cn(
'flex w-full flex-col items-center justify-center gap-2 rounded-lg border border-gray-200 p-2 py-6',
isSelected
? 'border-gray-200 bg-gray-100'
: 'text-gray-400 hover:bg-gray-50',
)}
onClick={onClick}
>
<Icon className="size-6" />
<span>{label}</span>
</button>
);
}
function GuideOptions() {
const [depth, setDepth] = useState<string>('essentials');
const depthSelectId = useId();
const depthOptions = [
{
label: 'Essentials',
value: 'essentials',
description: 'Just the code concepts',
},
{
label: 'Detailed',
value: 'detailed',
description: 'In-depth explanation',
},
{
label: 'Complete',
value: 'complete',
description: 'Cover the topic fully',
},
];
const selectedDepth = depthOptions.find((option) => option.value === depth);
return (
<div className="flex flex-col gap-2">
<label
htmlFor={depthSelectId}
className="inline-block text-sm text-gray-500"
>
Choose the guide options
</label>
<Select value={depth} onValueChange={setDepth}>
<SelectTrigger id={depthSelectId}>
{selectedDepth && (
<div className="flex flex-col gap-1">
<span>{selectedDepth.label}</span>
</div>
)}
{!selectedDepth && <SelectValue placeholder="Select a depth" />}
</SelectTrigger>
<SelectContent>
{depthOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div className="flex flex-col gap-1">
<span>{option.label}</span>
<span className="text-xs text-gray-500">
{option.description}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
}

185
src/components/Select.tsx Normal file
View File

@@ -0,0 +1,185 @@
import * as React from 'react';
import { Select as SelectPrimitive } from 'radix-ui';
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
import { cn } from '../lib/classname';
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
className,
size = 'default',
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: 'sm' | 'default';
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex h-9 w-full items-center justify-between gap-2 rounded-lg border bg-transparent px-3 py-2 text-sm whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectContent({
className,
children,
position = 'popper',
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
'relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg border bg-white shadow-md',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1',
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn('px-2 py-1.5 text-xs', className)}
{...props}
/>
);
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-gray-100 focus:text-black data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn(
'pointer-events-none -mx-1 my-1 h-px bg-gray-200',
className,
)}
{...props}
/>
);
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
'flex cursor-default items-center justify-center py-1',
className,
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
'flex cursor-default items-center justify-center py-1',
className,
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};

View File

@@ -4,6 +4,7 @@ import { CheckSubscriptionVerification } from '../../components/Billing/CheckSub
import { AICourse } from '../../components/GenerateCourse/AICourse';
import SkeletonLayout from '../../layouts/SkeletonLayout.astro';
import { AITutorLayout } from '../../components/AITutor/AITutorLayout';
import { ContentGenerator } from '../../components/ContentGenerator/ContentGenerator';
const ogImage = 'https://roadmap.sh/og-images/ai-tutor.png';
---
@@ -14,7 +15,8 @@ const ogImage = 'https://roadmap.sh/og-images/ai-tutor.png';
description='Learn anything with AI Tutor. Pick a topic, choose a difficulty level and the AI will guide you through the learning process.'
>
<AITutorLayout activeTab='new' client:load>
<AICourse client:load />
<ContentGenerator client:load />
<!-- <AICourse client:load /> -->
<CheckSubscriptionVerification client:load />
</AITutorLayout>
</SkeletonLayout>