mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-09 00:30:40 +02:00
wip
This commit is contained in:
@@ -72,6 +72,7 @@
|
|||||||
"npm-check-updates": "^18.0.1",
|
"npm-check-updates": "^18.0.1",
|
||||||
"playwright": "^1.52.0",
|
"playwright": "^1.52.0",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
|
"radix-ui": "^1.4.2",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-calendar-heatmap": "^1.10.0",
|
"react-calendar-heatmap": "^1.10.0",
|
||||||
"react-confetti": "^6.4.0",
|
"react-confetti": "^6.4.0",
|
||||||
|
1010
pnpm-lock.yaml
generated
1010
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
183
src/components/ContentGenerator/ContentGenerator.tsx
Normal file
183
src/components/ContentGenerator/ContentGenerator.tsx
Normal 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
185
src/components/Select.tsx
Normal 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,
|
||||||
|
};
|
@@ -4,6 +4,7 @@ import { CheckSubscriptionVerification } from '../../components/Billing/CheckSub
|
|||||||
import { AICourse } from '../../components/GenerateCourse/AICourse';
|
import { AICourse } from '../../components/GenerateCourse/AICourse';
|
||||||
import SkeletonLayout from '../../layouts/SkeletonLayout.astro';
|
import SkeletonLayout from '../../layouts/SkeletonLayout.astro';
|
||||||
import { AITutorLayout } from '../../components/AITutor/AITutorLayout';
|
import { AITutorLayout } from '../../components/AITutor/AITutorLayout';
|
||||||
|
import { ContentGenerator } from '../../components/ContentGenerator/ContentGenerator';
|
||||||
const ogImage = 'https://roadmap.sh/og-images/ai-tutor.png';
|
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.'
|
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>
|
<AITutorLayout activeTab='new' client:load>
|
||||||
<AICourse client:load />
|
<ContentGenerator client:load />
|
||||||
|
<!-- <AICourse client:load /> -->
|
||||||
<CheckSubscriptionVerification client:load />
|
<CheckSubscriptionVerification client:load />
|
||||||
</AITutorLayout>
|
</AITutorLayout>
|
||||||
</SkeletonLayout>
|
</SkeletonLayout>
|
||||||
|
Reference in New Issue
Block a user