├── .dockerignore ├── .env.example ├── .eslintrc.json ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── components.json ├── docker-compose.yml ├── next.config.mjs ├── package.json ├── pfp_seed.js ├── pnpm-lock.yaml ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── images │ ├── badge-launch.svg │ ├── badge-month.svg │ ├── badge-week.svg │ ├── repository-screenshot.png │ ├── roadmapai-screenshot.png │ └── wikipedia.png ├── opengraph-image.png └── roadmapai.svg ├── src ├── actions │ ├── roadmaps.ts │ └── users.ts ├── app │ ├── (auth) │ │ ├── layout.tsx │ │ ├── sign-in │ │ │ └── [[...sign-in]] │ │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── [[...sign-up]] │ │ │ └── page.tsx │ ├── (private) │ │ ├── dashboard │ │ │ ├── layout.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ └── roadmap │ │ │ ├── [id] │ │ │ └── page.tsx │ │ │ └── page.tsx │ ├── (public) │ │ └── explore │ │ │ └── page.tsx │ ├── api │ │ ├── health │ │ │ └── route.ts │ │ ├── og │ │ │ └── [title] │ │ │ │ ├── base-og.png │ │ │ │ └── route.tsx │ │ ├── v1 │ │ │ ├── cohere │ │ │ │ ├── details │ │ │ │ │ └── route.ts │ │ │ │ └── roadmap │ │ │ │ │ └── route.ts │ │ │ ├── gemini │ │ │ │ ├── details │ │ │ │ │ └── route.ts │ │ │ │ └── roadmap │ │ │ │ │ └── route.ts │ │ │ ├── groq │ │ │ │ ├── details │ │ │ │ │ └── route.ts │ │ │ │ └── roadmap │ │ │ │ │ └── route.ts │ │ │ ├── openai │ │ │ │ ├── details │ │ │ │ │ └── route.ts │ │ │ │ └── roadmap │ │ │ │ │ └── route.ts │ │ │ ├── orilley │ │ │ │ └── route.ts │ │ │ └── roadmaps │ │ │ │ └── route.ts │ │ └── webhook │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── opengraph-image.png │ └── page.tsx ├── components │ ├── ApiKeyDialog.tsx │ ├── alerts │ │ ├── EmptyAlert.tsx │ │ ├── ErrorAlert.tsx │ │ └── SearchAlert.tsx │ ├── app │ │ ├── appbar.tsx │ │ ├── icons.tsx │ │ ├── mobile-drawer.tsx │ │ ├── nav-items.tsx │ │ └── providers.tsx │ ├── flow-components │ │ ├── Flow.tsx │ │ ├── Instructions.tsx │ │ ├── Search.tsx │ │ ├── custom-node.tsx │ │ ├── data.js │ │ ├── drawer.tsx │ │ ├── expand-collapse.tsx │ │ ├── explore-pagination.tsx │ │ ├── generate-button.tsx │ │ ├── generator-controls.tsx │ │ ├── model-select.tsx │ │ ├── react-flow-pro.tsx │ │ └── roadmap-card.tsx │ ├── landing │ │ ├── roadmap-confetti.tsx │ │ ├── roadmap-features.tsx │ │ ├── roadmap-footer.tsx │ │ ├── roadmap-hero.tsx │ │ ├── roadmap-icon-cloud.tsx │ │ ├── roadmap-pricing.tsx │ │ ├── roadmap-stats.tsx │ │ └── roadmap-team.tsx │ ├── magicui │ │ ├── dot-pattern.tsx │ │ └── icon-cloud.tsx │ ├── marketing │ │ ├── banner.tsx │ │ ├── cta.tsx │ │ └── text-ticker.tsx │ ├── roadmap │ │ ├── code-viewer.tsx │ │ ├── data │ │ │ ├── models.ts │ │ │ └── presets.ts │ │ ├── maxlength-selector.tsx │ │ ├── model-selector.tsx │ │ ├── preset-save.tsx │ │ ├── preset-selector.tsx │ │ ├── preset-share.tsx │ │ ├── roadmap.tsx │ │ ├── temperature-selector.tsx │ │ └── top-p-selector.tsx │ ├── testimonials │ │ └── roadmap-testimonial.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── checkbox.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── dot-pattern.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── marque-wrapper.tsx │ │ ├── marquee.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── neobrutalism-button.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── skeleton.tsx │ │ ├── skew-scroll.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── three-d-button.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip.tsx │ │ └── use-toast.ts ├── fonts │ ├── inter │ │ ├── Inter-Black.ttf │ │ ├── Inter-Bold.ttf │ │ ├── Inter-ExtraBold.ttf │ │ ├── Inter-ExtraLight.ttf │ │ ├── Inter-Light.ttf │ │ ├── Inter-Medium.ttf │ │ ├── Inter-Regular.ttf │ │ ├── Inter-SemiBold.ttf │ │ ├── Inter-Thin.ttf │ │ └── Inter-VariableFont.ttf │ └── nunito │ │ ├── Nunito-Black.ttf │ │ ├── Nunito-BlackItalic.ttf │ │ ├── Nunito-Bold.ttf │ │ ├── Nunito-BoldItalic.ttf │ │ ├── Nunito-ExtraBold.ttf │ │ ├── Nunito-ExtraBoldItalic.ttf │ │ ├── Nunito-ExtraLight.ttf │ │ ├── Nunito-ExtraLightItalic.ttf │ │ ├── Nunito-Italic.ttf │ │ ├── Nunito-Light.ttf │ │ ├── Nunito-LightItalic.ttf │ │ ├── Nunito-Medium.ttf │ │ ├── Nunito-MediumItalic.ttf │ │ ├── Nunito-Regular.ttf │ │ ├── Nunito-SemiBold.ttf │ │ └── Nunito-SemiBoldItalic.ttf ├── hooks │ ├── use-animated-nodes.ts │ ├── use-has-mounted.ts │ └── use-mutation-observer.ts ├── lib │ ├── crisp.ts │ ├── db.ts │ ├── flow.ts │ ├── knowledge-graph.ts │ ├── queries.ts │ ├── shared │ │ ├── constants.ts │ │ ├── temp-data.ts │ │ ├── tree.js │ │ ├── types │ │ │ └── common.ts │ │ └── urls.ts │ ├── stores │ │ ├── index.ts │ │ ├── useFeatureFlag.ts │ │ └── useUI.ts │ ├── types.ts │ ├── utils.ts │ ├── validations │ │ ├── og.ts │ │ └── roadmap.ts │ └── youtube.ts └── middleware.ts ├── tailwind.config.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | node_modules 4 | npm-debug.log 5 | README.md 6 | .next 7 | .git -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # NOTE: This file is an example of the .env file that should be created in the root of the project. 2 | # To run the project locally in development keep the development environment variables and comment out the production environment variables. 3 | 4 | # DEVELOPMENT ENVIRONMENT VARIABLES 5 | 6 | # Database 7 | DATABASE_URL="" 8 | 9 | # API Keys 10 | OPENAI_API_KEY="" 11 | GEMINI_API_KEY="" 12 | COHERE_API_KEY="" 13 | GROQ_API_KEY="" 14 | 15 | # Youtube 16 | YOUTUBE_API_KEY="" 17 | 18 | # Crisp Chat Keys 19 | NEXT_PUBLIC_CRISP_WEBSITE_ID="" 20 | 21 | # Clerk Development Keys 22 | CLERK_SECRET_KEY="" 23 | WEBHOOK_SECRET="" 24 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="" 25 | NEXT_PUBLIC_CLERK_SIGN_IN_URL="/sign-in" 26 | NEXT_PUBLIC_CLERK_SIGN_UP_URL="/sign-up" 27 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL="/dashboard" 28 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL="/" 29 | 30 | # ======================================== 31 | 32 | # PRODUCTION ENVIRONMENT VARIABLES 33 | 34 | # Database 35 | DATABASE_URL="" 36 | 37 | # API Keys 38 | OPENAI_API_KEY="" 39 | GEMINI_API_KEY="" 40 | COHERE_API_KEY="" 41 | GROQ_API_KEY="" 42 | 43 | # Youtube 44 | YOUTUBE_API_KEY="" 45 | 46 | # Crisp Chat Keys 47 | NEXT_PUBLIC_CRISP_WEBSITE_ID="" 48 | 49 | # Clerk Production Keys 50 | CLERK_SECRET_KEY="" 51 | WEBHOOK_SECRET="" 52 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="" 53 | NEXT_PUBLIC_CLERK_SIGN_IN_URL="/sign-in" 54 | NEXT_PUBLIC_CLERK_SIGN_UP_URL="/sign-up" 55 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL="/dashboard" 56 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL="/" 57 | 58 | # Portainer stack gitops webhook 59 | WEBHOOK_URL="your-webook-url" 60 | 61 | # Docker image URL 62 | DOCKER_IMAGE_URL="your-docker-image-url" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env 30 | .env*.local 31 | .env*.development 32 | .env*.production 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | 41 | # prisma migrations 42 | prisma/migrations 43 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine AS base 2 | 3 | # Install dependencies only when needed 4 | FROM base AS deps 5 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 6 | RUN apk add --no-cache libc6-compat 7 | WORKDIR /app 8 | 9 | # Install dependencies based on the preferred package manager 10 | COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ 11 | RUN \ 12 | if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ 13 | elif [ -f package-lock.json ]; then npm ci; \ 14 | elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ 15 | else echo "Lockfile not found." && exit 1; \ 16 | fi 17 | 18 | 19 | # Rebuild the source code only when needed 20 | FROM base AS builder 21 | WORKDIR /app 22 | COPY --from=deps /app/node_modules ./node_modules 23 | COPY . . 24 | 25 | # Next.js collects completely anonymous telemetry data about general usage. 26 | # Learn more here: https://nextjs.org/telemetry 27 | # Uncomment the following line in case you want to disable telemetry during the build. 28 | # ENV NEXT_TELEMETRY_DISABLED 1 29 | 30 | RUN \ 31 | if [ -f yarn.lock ]; then yarn run build; \ 32 | elif [ -f package-lock.json ]; then npm run build; \ 33 | elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ 34 | else echo "Lockfile not found." && exit 1; \ 35 | fi 36 | 37 | # Production image, copy all the files and run next 38 | FROM base AS runner 39 | WORKDIR /app 40 | 41 | ENV NODE_ENV production 42 | # Uncomment the following line in case you want to disable telemetry during runtime. 43 | # ENV NEXT_TELEMETRY_DISABLED 1 44 | 45 | RUN addgroup --system --gid 1001 nodejs 46 | RUN adduser --system --uid 1001 nextjs 47 | 48 | COPY --from=builder /app/public ./public 49 | 50 | # Set the correct permission for prerender cache 51 | RUN mkdir .next 52 | RUN chown nextjs:nodejs .next 53 | 54 | # Automatically leverage output traces to reduce image size 55 | # https://nextjs.org/docs/advanced-features/output-file-tracing 56 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ 57 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 58 | 59 | USER nextjs 60 | 61 | EXPOSE 3000 62 | 63 | ENV PORT 3000 64 | 65 | # server.js is created by next build from the standalone output 66 | # https://nextjs.org/docs/pages/api-reference/next-config-js/output 67 | CMD HOSTNAME="0.0.0.0" node server.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 AI Roadmap Generator 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Roadmap AI 2 | 3 | ![opengraph-image](https://github.com/thatbeautifuldream/ai-roadmap-generator/assets/28717686/3c9c981f-364e-4de0-9016-b8d9cf147b43) 4 | 5 | ## Introduction 6 | 7 | This project generates learning roadmaps for given search queries. For example, if a user searches for "machine learning", the project will generate a learning roadmap for machine learning. The roadmap will include the most important topics, resources, and learning paths for machine learning. 8 | 9 | ## Setting up the project locally 10 | 11 | - We use `pnpm` for package management. You can install `pnpm` by following the instructions [here](https://pnpm.io/installation). 12 | - To install the dependencies, run `pnpm install`. 13 | - To start the development server, run `pnpm dev`. 14 | - To build the project, run `pnpm build`. 15 | - To start the production server, run `pnpm start`. 16 | 17 | ## Features 18 | 19 | - Multimodel Support . 20 | - Share roadmap via public URL. 21 | - Recommended Orilley Books. 22 | 23 | ## Building and pusing the docker image to `ghcr.io` from the local machine 24 | 25 | ```bash 26 | docker login ghcr.io 27 | docker build . -t ghcr.io/thatbeautifuldream/ai-roadmap-generator:latest 28 | docker push ghcr.io/thatbeautifuldream/ai-roadmap-generator:latest 29 | ``` 30 | 31 | ## Pulling and running docker image on the server 32 | 33 | ```bash 34 | docker pull ghcr.io/thatbeautifuldream/ai-roadmap-generator:latest 35 | docker run -d -p 3000:3000 --name ai-roadmap-generator ghcr.io/thatbeautifuldream/ai-roadmap-generator:latest 36 | ``` 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | ai-roadmap-generator: 4 | image: ghcr.io/thatbeautifuldream/ai-roadmap-generator:latest 5 | container_name: ai-roadmap-generator 6 | ports: 7 | - 3000:3000 8 | restart: always 9 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | async redirects() { 4 | return [ 5 | { 6 | source: "/twitter", 7 | destination: "https://twitter.com/airoadmapgen", 8 | permanent: true, 9 | }, 10 | { 11 | source: "/discord", 12 | destination: "https://discord.gg/2rMV53UqYB", 13 | permanent: true, 14 | }, 15 | ]; 16 | }, 17 | experimental: { 18 | typedRoutes: true, 19 | }, 20 | output: "standalone", 21 | }; 22 | 23 | export default nextConfig; 24 | -------------------------------------------------------------------------------- /pfp_seed.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require("@prisma/client"); 2 | const { clerkClient } = require("@clerk/nextjs"); 3 | 4 | const prisma = new PrismaClient(); 5 | 6 | async function updateImageUrls() { 7 | const dbUsers = await clerkClient.users.getUserList({ 8 | limit: 250, 9 | }); 10 | 11 | try { 12 | for (const dbUser of dbUsers) { 13 | if (dbUser.imageUrl) { 14 | await prisma.user.update({ 15 | where: { 16 | id: dbUser.id, 17 | }, 18 | data: { 19 | imageUrl: dbUser.imageUrl, 20 | }, 21 | }); 22 | console.log(`Updated image URL for user ${dbUser.firstName} ${dbUser.lastName}.`); 23 | } 24 | } 25 | 26 | console.log("Image URLs updated successfully."); 27 | } catch (error) { 28 | console.error("Error updating image URLs:", error); 29 | } finally { 30 | await prisma.$disconnect(); 31 | } 32 | } 33 | 34 | (async () => { 35 | await updateImageUrls(); 36 | })(); 37 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model User { 11 | id String @id @unique 12 | name String 13 | email String 14 | imageUrl String? 15 | credits Int @default(5) 16 | roadmaps Roadmap[] 17 | savedRoadmaps SavedRoadmap[] 18 | } 19 | 20 | enum Visibility { 21 | PRIVATE 22 | PUBLIC 23 | } 24 | 25 | model Roadmap { 26 | id String @id @default(cuid()) 27 | title String @unique 28 | content String 29 | userId String 30 | author User @relation(fields: [userId], references: [id], onDelete: Cascade) 31 | createdAt DateTime @default(now()) 32 | views Int @default(0) 33 | searchCount Int @default(0) 34 | visibility Visibility @default(PUBLIC) 35 | drawerDetails DrawerDetail[] 36 | 37 | @@index([title]) 38 | } 39 | 40 | model SavedRoadmap { 41 | id String @id @default(cuid()) 42 | title String 43 | roadmapId String 44 | userId String 45 | author User @relation(fields: [userId], references: [id], onDelete: Cascade) 46 | } 47 | 48 | model DrawerDetail { 49 | id String @id @default(cuid()) 50 | details String 51 | youtubeVideoIds String[] 52 | books String 53 | nodeName String @unique 54 | roadmapId String 55 | roadmap Roadmap @relation(fields: [roadmapId], references: [id], onDelete: Cascade) 56 | } 57 | -------------------------------------------------------------------------------- /public/images/repository-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatbeautifuldream/ai-roadmap-generator/7c70d72c405ce01b1856510d222e8b3da6519fc4/public/images/repository-screenshot.png -------------------------------------------------------------------------------- /public/images/roadmapai-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatbeautifuldream/ai-roadmap-generator/7c70d72c405ce01b1856510d222e8b3da6519fc4/public/images/roadmapai-screenshot.png -------------------------------------------------------------------------------- /public/images/wikipedia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatbeautifuldream/ai-roadmap-generator/7c70d72c405ce01b1856510d222e8b3da6519fc4/public/images/wikipedia.png -------------------------------------------------------------------------------- /public/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatbeautifuldream/ai-roadmap-generator/7c70d72c405ce01b1856510d222e8b3da6519fc4/public/opengraph-image.png -------------------------------------------------------------------------------- /src/actions/users.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { db } from "@/lib/db"; 3 | import { getUserId } from "./roadmaps"; 4 | 5 | export const decrementCreditsByUserId = async () => { 6 | const userId = await getUserId(); 7 | try { 8 | // Retrieve the current user's credits 9 | const user = await db.user.findUnique({ 10 | where: { 11 | id: userId, 12 | }, 13 | }); 14 | 15 | // Check if user exists and has more than 0 credits 16 | if (user && user.credits > 0) { 17 | await db.user.update({ 18 | where: { 19 | id: userId, 20 | }, 21 | data: { 22 | credits: { 23 | decrement: 1, // use a positive number to indicate decrement 24 | }, 25 | }, 26 | }); 27 | return true; 28 | } 29 | // Either user does not exist or does not have enough credits to decrement 30 | return false; 31 | } catch (e) { 32 | // Handle the error, optionally log it or throw it 33 | console.error(e); 34 | return false; 35 | } 36 | }; 37 | 38 | export const userHasCredits = async () => { 39 | const userId = await getUserId(); 40 | const user = await db.user.findUnique({ 41 | where: { 42 | id: userId, 43 | }, 44 | select: { 45 | credits: true, 46 | }, 47 | }); 48 | 49 | if (user && user?.credits > 0) { 50 | return true; 51 | } 52 | return false; 53 | }; 54 | 55 | export const getUserCredits = async () => { 56 | const userId = await getUserId(); 57 | 58 | if (!userId) { 59 | return; 60 | } 61 | 62 | const user = await db.user.findUnique({ 63 | where: { 64 | id: userId, 65 | }, 66 | select: { 67 | credits: true, 68 | }, 69 | }); 70 | 71 | return user?.credits; 72 | }; 73 | 74 | export const getTotalUsers = async () => { 75 | const totalUsers = await db.user.count(); 76 | return totalUsers; 77 | }; 78 | -------------------------------------------------------------------------------- /src/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type Props = { 4 | children: React.ReactNode; 5 | }; 6 | 7 | export default function AuthLayout(props: Props) { 8 | const { children } = props; 9 | return ( 10 |
11 | {children} 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/(private)/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function DashboardLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return ( 7 |
8 |
{children}
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/app/(private)/dashboard/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | 3 | const LoadingSkeleton = ({ width }: { width: string }) => ( 4 | 8 |

9 | 10 |

11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | ); 21 | 22 | const Loading = () => { 23 | const items = [ 24 | { id: 1, width: "320px" }, 25 | { id: 2, width: "320px" }, 26 | { id: 3, width: "320px" }, 27 | { id: 4, width: "320px" }, 28 | { id: 5, width: "320px" }, 29 | { id: 6, width: "320px" }, 30 | ]; 31 | 32 | return ( 33 |
34 |
35 | {items.map((item) => ( 36 | 37 | ))} 38 |
39 |
40 | ); 41 | }; 42 | 43 | export default Loading; 44 | -------------------------------------------------------------------------------- /src/app/(private)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | getRoadmapsByUserId, 3 | getSavedRoadmapsByUserId, 4 | } from "@/actions/roadmaps"; 5 | import { EmptyAlert } from "@/components/alerts/EmptyAlert"; 6 | import RoadmapCard from "@/components/flow-components/roadmap-card"; 7 | import { timeFromNow } from "@/lib/utils"; 8 | 9 | export default async function Dashboard() { 10 | const roadmaps = await getRoadmapsByUserId(); 11 | const savedRoadmaps = await getSavedRoadmapsByUserId(); 12 | 13 | return ( 14 |
15 |
16 |

17 | Your Roadmaps 18 |

19 |
20 |
21 | {roadmaps.length > 0 && 22 | roadmaps.map((roadmap) => ( 23 | 31 | ))} 32 | {roadmaps.length === 0 && ( 33 |
34 | 38 |
39 | )} 40 |
41 |
42 |

43 | Saved Roadmaps 44 |

45 |
46 |
47 | {savedRoadmaps.length > 0 && 48 | savedRoadmaps.map((roadmap) => ( 49 | 56 | ))} 57 | {savedRoadmaps.length === 0 && ( 58 |
59 | 63 |
64 | )} 65 |
66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/app/(private)/roadmap/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getRoadmapById, increaseViewsByRoadmapId } from "@/actions/roadmaps"; 2 | import { ErrorAlert } from "@/components/alerts/ErrorAlert"; 3 | import { Flow } from "@/components/flow-components/Flow"; 4 | 5 | type PageProps = { 6 | params: { id: string }; 7 | searchParams: {}; 8 | }; 9 | 10 | const generatorById = async (props: PageProps) => { 11 | const { 12 | params: { id: roadmapId }, 13 | } = props; 14 | const roadmap = await getRoadmapById(roadmapId); 15 | await increaseViewsByRoadmapId(roadmapId); 16 | 17 | if (!roadmap) return ; 18 | return ( 19 | <> 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default generatorById; 26 | -------------------------------------------------------------------------------- /src/app/(private)/roadmap/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Roadmap from "@/components/roadmap/roadmap"; 3 | import { Suspense } from "react"; 4 | import { ReactFlowProvider } from "reactflow"; 5 | 6 | export default function Home() { 7 | return ( 8 | }> 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/app/(public)/explore/page.tsx: -------------------------------------------------------------------------------- 1 | import Search from "@/components/flow-components/Search"; 2 | 3 | export default function Explore() { 4 | return ( 5 |
6 |
7 |
8 |

9 | Explore Roadmaps 10 |

11 | 12 |
13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/app/api/health/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | export const GET = async (req: Request, res: Response) => { 4 | return NextResponse.json( 5 | { status: true, message: "ok" }, 6 | { status: 200 } 7 | ); 8 | }; 9 | -------------------------------------------------------------------------------- /src/app/api/og/[title]/base-og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatbeautifuldream/ai-roadmap-generator/7c70d72c405ce01b1856510d222e8b3da6519fc4/src/app/api/og/[title]/base-og.png -------------------------------------------------------------------------------- /src/app/api/og/[title]/route.tsx: -------------------------------------------------------------------------------- 1 | import { ImageResponse } from "next/og"; 2 | import { z } from "zod"; 3 | 4 | export const runtime = "edge"; 5 | 6 | export type Props = { 7 | title?: string; 8 | }; 9 | 10 | function arrayBufferToBase64(buffer: ArrayBuffer) { 11 | let binary = ""; 12 | const bytes = [].slice.call(new Uint8Array(buffer)); 13 | bytes.forEach((b) => (binary += String.fromCharCode(b))); 14 | const myBuffer = Buffer.from(binary, "binary"); // convert to buffer 15 | return myBuffer.toString("base64"); 16 | } 17 | async function getImageSrcFromPath(url: URL) { 18 | const image = await fetch(url).then((res) => res.arrayBuffer()); 19 | // get image src from arraybuffer 20 | // https://stackoverflow.com/a/18650249/3015595 21 | const base64Flag = "data:image/png;base64,"; 22 | const imageStr = arrayBufferToBase64(image); 23 | return base64Flag + imageStr; 24 | } 25 | 26 | const paramsSchema = z.object({ 27 | title: z.string(), 28 | }); 29 | 30 | export async function GET( 31 | request: Request, 32 | { params }: { params: unknown } 33 | ): Promise { 34 | const { title: titleParam } = paramsSchema.parse(params); 35 | const title = decodeURIComponent(titleParam); 36 | // turn the first letter of the title capitalized 37 | const text = title.charAt(0).toUpperCase() + title.slice(1); 38 | const [docsImageSrc] = await Promise.all([ 39 | getImageSrcFromPath(new URL("./base-og.png", import.meta.url)), 40 | ]); 41 | 42 | const nunitoSemiBold = fetch( 43 | new URL("@/fonts/nunito/Nunito-SemiBold.ttf", import.meta.url) 44 | ).then((res) => res.arrayBuffer()); 45 | return new ImageResponse( 46 | ( 47 |
56 |
57 | image 58 |
59 |

{text}

60 |
61 |
62 |
63 | ), 64 | { 65 | width: 1686, 66 | height: 882, 67 | fonts: [ 68 | { 69 | name: "Nunito", 70 | data: await nunitoSemiBold, 71 | style: "normal", 72 | weight: 400, 73 | }, 74 | ], 75 | } 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/app/api/v1/cohere/details/route.ts: -------------------------------------------------------------------------------- 1 | import { JSONType } from "@/lib/types"; 2 | import { SanitiseJSON } from "@/lib/utils"; 3 | import { ChatCohere } from "@langchain/cohere"; 4 | import { ChatPromptTemplate } from "@langchain/core/prompts"; 5 | import { NextRequest, NextResponse } from "next/server"; 6 | 7 | export const POST = async (req: NextRequest) => { 8 | try { 9 | const apiKey = req.nextUrl.searchParams.get("apiKey"); 10 | const roadmapId = req.nextUrl.searchParams.get("roadmapId"); 11 | const body = await req.json(); 12 | const query = body.query; 13 | const child = body.child; 14 | const parent = body.parent; 15 | 16 | if (!roadmapId) { 17 | return NextResponse.json( 18 | { status: false, message: "Please send required params." }, 19 | { status: 400 }, 20 | ); 21 | } 22 | 23 | if (!query || !child || !parent) { 24 | return NextResponse.json( 25 | { status: false, message: "Please send required params." }, 26 | { status: 400 }, 27 | ); 28 | } 29 | 30 | const model = new ChatCohere({ 31 | apiKey: apiKey || process.env.COHERE_API_KEY, 32 | model: "command", 33 | }); 34 | 35 | const prompt = ChatPromptTemplate.fromMessages([ 36 | [ 37 | "ai", 38 | "You are a helpful AI assistant that can generate career/syllabus roadmap. ", 39 | ], 40 | ["human", "{input}"], 41 | ]); 42 | const chain = prompt.pipe(model); 43 | const response = await chain.invoke({ 44 | input: `A roadmap in JSON format has been generated related to the title: ${query} which has the JSON structure: {query: ${query}, chapters: {chapterName: [{moduleName: string, moduleDescription: string, link?: string}]}} and i'd like to request a small description in markdown format and as bullet points (minimum 5) and wikipedia link on moduleName: ${child} from chapterName: ${parent} in JSON format: {description: string, link: string, bulletPoints: string[]}`, 45 | }); 46 | 47 | let json: JSONType | null = null; 48 | 49 | try { 50 | const data = SanitiseJSON(String(response?.content)); 51 | json = JSON.parse(data); 52 | 53 | if (!json) { 54 | return NextResponse.json( 55 | { 56 | status: false, 57 | message: "Error parsing roadmap data.", 58 | }, 59 | { status: 500 }, 60 | ); 61 | } 62 | 63 | return NextResponse.json({ status: true, text: json }, { status: 200 }); 64 | } catch (e) { 65 | console.log(e); 66 | return NextResponse.json( 67 | { 68 | status: false, 69 | message: "Error parsing roadmap data.", 70 | }, 71 | { status: 500 }, 72 | ); 73 | } 74 | } catch (e) { 75 | console.log(e); 76 | return NextResponse.json( 77 | { status: false, message: "Something went wrong." }, 78 | { status: 400 }, 79 | ); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /src/app/api/v1/gemini/details/route.ts: -------------------------------------------------------------------------------- 1 | import { SanitiseJSON } from "@/lib/utils"; 2 | import { HarmBlockThreshold, HarmCategory } from "@google/generative-ai"; 3 | import { ChatGoogleGenerativeAI } from "@langchain/google-genai"; 4 | import { NextRequest, NextResponse } from "next/server"; 5 | 6 | export const POST = async (req: NextRequest) => { 7 | try { 8 | const apiKey = req.nextUrl.searchParams.get("apiKey"); 9 | const roadmapId = req.nextUrl.searchParams.get("roadmapId"); 10 | const body = await req.json(); 11 | const query = body.query; 12 | const child = body.child; 13 | const parent = body.parent; 14 | 15 | if (!roadmapId) { 16 | return NextResponse.json( 17 | { status: false, message: "Please send required params." }, 18 | { status: 400 }, 19 | ); 20 | } 21 | 22 | if (!query || !child || !parent) { 23 | return NextResponse.json( 24 | { status: false, message: "Please send required params." }, 25 | { status: 400 }, 26 | ); 27 | } 28 | 29 | const model = new ChatGoogleGenerativeAI({ 30 | modelName: "gemini-pro", 31 | maxOutputTokens: 2048, 32 | apiKey: apiKey || process.env.GEMINI_API_KEY, 33 | safetySettings: [ 34 | { 35 | category: HarmCategory.HARM_CATEGORY_HARASSMENT, 36 | threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE, 37 | }, 38 | ], 39 | }); 40 | 41 | const response = await model.invoke([ 42 | [ 43 | "system", 44 | "You are a helpful AI assistant that can generate career/syllabus roadmap.", 45 | ], 46 | [ 47 | "human", 48 | `a roadmap in JSON format has been generated related to the title: ${query} which has the JSON structure: {query: ${query}, chapters: {chapterName: [{moduleName: string, moduleDescription: string, link?: string}]}} and i'd like to request a small description and wikipedia link on moduleName: ${child} from chapterName: ${parent} in JSON format: {description: string, link: string}`, 49 | ], 50 | ]); 51 | let json: { description: string; link: string } | null = null; 52 | 53 | try { 54 | json = JSON.parse(SanitiseJSON(String(response.content)) || ""); 55 | 56 | if (!json) { 57 | return NextResponse.json( 58 | { 59 | status: false, 60 | message: "Error parsing roadmap data.", 61 | }, 62 | { status: 500 }, 63 | ); 64 | } 65 | return NextResponse.json({ status: true, text: json }, { status: 200 }); 66 | } catch (e) { 67 | console.log(e); 68 | return NextResponse.json( 69 | { 70 | status: false, 71 | message: "Error parsing roadmap data.", 72 | }, 73 | { status: 500 }, 74 | ); 75 | } 76 | } catch (e) { 77 | console.log(e); 78 | return NextResponse.json( 79 | { status: false, message: "Something went wrong." }, 80 | { status: 400 }, 81 | ); 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /src/app/api/v1/groq/details/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { OpenAI } from "openai"; 3 | 4 | export const POST = async (req: NextRequest) => { 5 | try { 6 | const apiKey = req.nextUrl.searchParams.get("apiKey"); 7 | const roadmapId = req.nextUrl.searchParams.get("roadmapId"); 8 | 9 | if (!roadmapId) { 10 | return NextResponse.json( 11 | { status: false, message: "Please send required params." }, 12 | { status: 400 }, 13 | ); 14 | } 15 | 16 | const openai = new OpenAI({ 17 | apiKey: apiKey || process.env.GROQ_API_KEY, 18 | baseURL: "https://api.groq.com/openai/v1", 19 | }); 20 | 21 | const body = await req.json(); 22 | const query = body.query; 23 | const child = body.child; 24 | const parent = body.parent; 25 | 26 | if (!query || !child || !parent) { 27 | return NextResponse.json( 28 | { status: false, message: "Please send required params." }, 29 | { status: 400 }, 30 | ); 31 | } 32 | 33 | const text = await openai.chat.completions.create({ 34 | model: "mixtral-8x7b-32768", 35 | temperature: 1, 36 | messages: [ 37 | { 38 | role: "system", 39 | content: `You are a helpful AI assistant that can generate career/syllabus roadmap.`, 40 | }, 41 | { 42 | role: "user", 43 | content: `a roadmap in JSON format has been generated related to the title: ${query} which has the JSON structure: {query: ${query}, chapters: {chapterName: [{moduleName: string, moduleDescription: string, link?: string}]}} and i'd like to request a small description and wikipedia link on moduleName: ${child} from chapterName: ${parent} in JSON format: {description: string, link: string}`, 44 | }, 45 | ], 46 | response_format: { type: "json_object" }, 47 | }); 48 | 49 | let json: { description: string; link: string } | null = null; 50 | 51 | try { 52 | json = JSON.parse(text?.choices?.[0]?.message?.content || ""); 53 | 54 | if (!json) { 55 | return NextResponse.json( 56 | { 57 | status: false, 58 | message: "Error parsing roadmap data.", 59 | }, 60 | { status: 500 }, 61 | ); 62 | } 63 | 64 | return NextResponse.json({ status: true, text: json }, { status: 200 }); 65 | } catch (e) { 66 | console.log(e); 67 | return NextResponse.json( 68 | { 69 | status: false, 70 | message: "Error parsing roadmap data.", 71 | }, 72 | { status: 500 }, 73 | ); 74 | } 75 | } catch (e) { 76 | console.log(e); 77 | return NextResponse.json( 78 | { status: false, message: "Something went wrong." }, 79 | { status: 400 }, 80 | ); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /src/app/api/v1/openai/details/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { OpenAI } from "openai"; 3 | 4 | export const POST = async (req: NextRequest) => { 5 | try { 6 | const apiKey = req.nextUrl.searchParams.get("apiKey"); 7 | const roadmapId = req.nextUrl.searchParams.get("roadmapId"); 8 | 9 | if (!roadmapId) { 10 | return NextResponse.json( 11 | { status: false, message: "Please send required params." }, 12 | { status: 400 }, 13 | ); 14 | } 15 | 16 | const openai = new OpenAI({ 17 | apiKey: apiKey || process.env.OPENAI_API_KEY, 18 | }); 19 | 20 | const body = await req.json(); 21 | const query = body.query; 22 | const child = body.child; 23 | const parent = body.parent; 24 | 25 | if (!query || !child || !parent) { 26 | return NextResponse.json( 27 | { status: false, message: "Please send required params." }, 28 | { status: 400 }, 29 | ); 30 | } 31 | 32 | const text = await openai.chat.completions.create({ 33 | model: "gpt-3.5-turbo-1106", 34 | temperature: 1, 35 | messages: [ 36 | { 37 | role: "system", 38 | content: `You are a helpful AI assistant that can generate career/syllabus roadmap.`, 39 | }, 40 | { 41 | role: "user", 42 | content: `a roadmap in JSON format has been generated related to the title: ${query} which has the JSON structure: {query: ${query}, chapters: {chapterName: [{moduleName: string, moduleDescription: string, link?: string}]}} and i'd like to request a small description and wikipedia link on moduleName: ${child} from chapterName: ${parent} in JSON format: {description: string, link: string}`, 43 | }, 44 | ], 45 | response_format: { type: "json_object" }, 46 | }); 47 | 48 | let json: { description: string; link: string } | null = null; 49 | 50 | try { 51 | json = JSON.parse(text?.choices?.[0]?.message?.content || ""); 52 | 53 | if (!json) { 54 | return NextResponse.json( 55 | { 56 | status: false, 57 | message: "Error parsing roadmap data.", 58 | }, 59 | { status: 500 }, 60 | ); 61 | } 62 | 63 | return NextResponse.json({ status: true, text: json }, { status: 200 }); 64 | } catch (e) { 65 | console.log(e); 66 | return NextResponse.json( 67 | { 68 | status: false, 69 | message: "Error parsing roadmap data.", 70 | }, 71 | { status: 500 }, 72 | ); 73 | } 74 | } catch (e) { 75 | console.log(e); 76 | return NextResponse.json( 77 | { status: false, message: "Something went wrong." }, 78 | { status: 400 }, 79 | ); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /src/app/api/v1/orilley/route.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export const POST = async (req: Request, res: Response) => { 5 | try { 6 | const body = await req.json(); 7 | const query = body.data.query; 8 | 9 | if (!query) { 10 | return NextResponse.json( 11 | { status: false, message: "Please send required params." }, 12 | { status: 400 } 13 | ); 14 | } 15 | 16 | const { data } = await axios.get( 17 | `https://learning.oreilly.com/api/v2/search/?query=${query}&formats=book&limit=2` 18 | ); 19 | return NextResponse.json({ status: true, data }, { status: 200 }); 20 | } catch (e) { 21 | console.log(e); 22 | return NextResponse.json( 23 | { status: false, message: "Something went wrong." }, 24 | { status: 400 } 25 | ); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/app/api/v1/roadmaps/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | export const POST = async (req: NextRequest, res: NextResponse) => { 5 | try { 6 | const body = await req.json(); 7 | const searchString = body.query; 8 | const searchResults = await db.roadmap.findMany({ 9 | where: { 10 | title: { 11 | contains: searchString, 12 | }, 13 | visibility: "PUBLIC", 14 | }, 15 | }); 16 | 17 | return NextResponse.json( 18 | { status: true, data: searchResults }, 19 | { status: 200 }, 20 | ); 21 | } catch (e) { 22 | console.log(e); 23 | return NextResponse.json( 24 | { status: false, message: "bad reqe" }, 25 | { status: 500 }, 26 | ); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/app/api/webhook/route.ts: -------------------------------------------------------------------------------- 1 | import { Webhook } from "svix"; 2 | import { headers } from "next/headers"; 3 | import { UserJSON, WebhookEvent } from "@clerk/nextjs/server"; 4 | import { db } from "@/lib/db"; 5 | 6 | export async function POST(req: Request) { 7 | const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; 8 | 9 | if (!WEBHOOK_SECRET) { 10 | throw new Error("WEBHOOK_SECRET is not set"); 11 | } 12 | 13 | // Get the headers 14 | const headerPayload = headers(); 15 | const svix_id = headerPayload.get("svix-id"); 16 | const svix_signature = headerPayload.get("svix-signature"); 17 | const svix_timestamp = headerPayload.get("svix-timestamp"); 18 | 19 | // If there are no headers, error out. 20 | 21 | if (!svix_id || !svix_signature || !svix_timestamp) { 22 | return new Response("Error occured -- no svix headers", { 23 | status: 400, 24 | }); 25 | } 26 | 27 | // Get the body. 28 | const payload = await req.json(); 29 | const body = JSON.stringify(payload); 30 | 31 | // Create a new SVIX instance with the secret. 32 | const wh = new Webhook(WEBHOOK_SECRET); 33 | 34 | let evt: WebhookEvent; 35 | 36 | // Verify the payload with the headers. 37 | 38 | try { 39 | evt = wh.verify(body, { 40 | "svix-id": svix_id, 41 | "svix-timestamp": svix_timestamp, 42 | "svix-signature": svix_signature, 43 | }) as WebhookEvent; 44 | } catch (err) { 45 | console.error("Error verifying webhook:", err); 46 | return new Response("Error occured", { 47 | status: 400, 48 | }); 49 | } 50 | 51 | // Get user details. 52 | const { id, first_name, last_name, email_addresses, image_url } = 53 | evt.data as UserJSON; 54 | 55 | // Create a new user in the database. 56 | try { 57 | const user = await db.user.create({ 58 | data: { 59 | id, 60 | name: `${first_name} ${last_name}`, 61 | email: email_addresses[0].email_address, 62 | imageUrl: image_url, 63 | }, 64 | }); 65 | 66 | return new Response("New User created", { status: 201 }); 67 | } catch (err) { 68 | console.log("Error creating user: ", err); 69 | return new Response("Error occured", { 70 | status: 500, 71 | }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatbeautifuldream/ai-roadmap-generator/7c70d72c405ce01b1856510d222e8b3da6519fc4/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 240 10% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --primary: 240 5.9% 10%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 240 4.8% 95.9%; 16 | --secondary-foreground: 240 5.9% 10%; 17 | --muted: 240 4.8% 95.9%; 18 | --muted-foreground: 240 3.8% 46.1%; 19 | --accent: 240 4.8% 95.9%; 20 | --accent-foreground: 240 5.9% 10%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 240 5.9% 90%; 24 | --input: 240 5.9% 90%; 25 | --ring: 240 5.9% 10%; 26 | --radius: 0.4rem; 27 | } 28 | 29 | .dark { 30 | --background: 240 10% 3.9%; 31 | --foreground: 0 0% 98%; 32 | --card: 240 10% 3.9%; 33 | --card-foreground: 0 0% 98%; 34 | --popover: 240 10% 3.9%; 35 | --popover-foreground: 0 0% 98%; 36 | --primary: 0 0% 98%; 37 | --primary-foreground: 240 5.9% 10%; 38 | --secondary: 240 3.7% 15.9%; 39 | --secondary-foreground: 0 0% 98%; 40 | --muted: 240 3.7% 15.9%; 41 | --muted-foreground: 240 5% 64.9%; 42 | --accent: 240 3.7% 15.9%; 43 | --accent-foreground: 0 0% 98%; 44 | --destructive: 0 62.8% 30.6%; 45 | --destructive-foreground: 0 0% 98%; 46 | --border: 240 3.7% 15.9%; 47 | --input: 240 3.7% 15.9%; 48 | --ring: 240 4.9% 83.9%; 49 | } 50 | } 51 | 52 | @layer base { 53 | * { 54 | @apply border-border; 55 | -ms-overflow-style: none; 56 | } 57 | ::-webkit-scrollbar { 58 | display: none; 59 | } 60 | body { 61 | @apply bg-background text-foreground; 62 | } 63 | } 64 | 65 | .react-flow__node { 66 | display: flex; 67 | cursor: pointer; 68 | justify-content: center; 69 | align-items: center; 70 | font-weight: 600; 71 | border-radius: 10px; 72 | border-width: 2px; 73 | box-shadow: 6px 6px 0 1px rgba(0, 0, 0, 0.65); 74 | } 75 | 76 | .react-flow__node.selected, 77 | .react-flow__node:hover, 78 | .react-flow__node:focus { 79 | box-shadow: 6px 6px 0 1px rgba(0, 0, 0, 0.65); 80 | background-color: #eee; 81 | } 82 | 83 | .skew-scroll { 84 | animation: skew-scroll 20s linear infinite; 85 | } 86 | 87 | /* .react-flow__node.selected { 88 | background-color: #ff0072; 89 | color: white; 90 | }*/ 91 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import AppBar from "@/components/app/appbar"; 2 | import Providers from "@/components/app/providers"; 3 | import type { Metadata } from "next"; 4 | import { Nunito } from "next/font/google"; 5 | import NextTopLoader from "nextjs-toploader"; 6 | import "./globals.css"; 7 | 8 | const nunito = Nunito({ subsets: ["latin"] }); 9 | 10 | export const metadata: Metadata = { 11 | metadataBase: new URL("https://www.airoadmapgenerator.com/"), 12 | title: "AI Roadmap Generator", 13 | description: "Generate your roadmaps with AI.", 14 | openGraph: { 15 | url: "https://www.airoadmapgenerator.com/", 16 | 17 | images: "/opengraph-image.png", 18 | }, 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: Readonly<{ 24 | children: React.ReactNode; 25 | }>) { 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | {children} 33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatbeautifuldream/ai-roadmap-generator/7c70d72c405ce01b1856510d222e8b3da6519fc4/src/app/opengraph-image.png -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import RoadmapHero from "@/components/landing/roadmap-hero"; 2 | import RoadmapFeatures from "@/components/landing/roadmap-features"; 3 | import RoadmapTestimonial from "@/components/testimonials/roadmap-testimonial"; 4 | import RoadmapTeam from "@/components/landing/roadmap-team"; 5 | import RoadmapFooter from "@/components/landing/roadmap-footer"; 6 | import RoadmapStats from "@/components/landing/roadmap-stats"; 7 | import CTA from "@/components/marketing/cta"; 8 | 9 | export default function Home() { 10 | return ( 11 | <> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/alerts/EmptyAlert.tsx: -------------------------------------------------------------------------------- 1 | interface EmptyAlertProps { 2 | description?: string; 3 | title?: string; 4 | } 5 | 6 | export function EmptyAlert({ title, description }: EmptyAlertProps) { 7 | return ( 8 |
9 |

{title}

10 |

{description}

11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/components/alerts/ErrorAlert.tsx: -------------------------------------------------------------------------------- 1 | import { AlertCircle } from "lucide-react"; 2 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; 3 | 4 | export function ErrorAlert() { 5 | return ( 6 | 7 | 8 | Error 9 | 10 | There was an error loading items, please try again. 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/alerts/SearchAlert.tsx: -------------------------------------------------------------------------------- 1 | import { AlertCircle } from "lucide-react"; 2 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; 3 | 4 | export function SearchAlert() { 5 | return ( 6 | 7 | 8 | Oops 9 | 10 | No items match your search. Please try a different query. 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/app/appbar.tsx: -------------------------------------------------------------------------------- 1 | import NavItems from "@/components/app/nav-items"; 2 | import { Badge } from "@/components/ui/badge"; 3 | import { buttonVariants } from "@/components/ui/button"; 4 | import { SignInButton, UserButton, currentUser } from "@clerk/nextjs"; 5 | import { Coins } from "lucide-react"; 6 | import { Link } from "next-view-transitions"; 7 | 8 | import { getUserCredits } from "@/actions/users"; 9 | import { 10 | Tooltip, 11 | TooltipContent, 12 | TooltipProvider, 13 | TooltipTrigger, 14 | } from "@/components/ui/tooltip"; 15 | import MobileDrawer from "@/components/app/mobile-drawer"; 16 | import NeobrutalismButton from "@/components/ui/neobrutalism-button"; 17 | import ThreeDButton from "../ui/three-d-button"; 18 | 19 | async function AppBar() { 20 | const user = await currentUser(); 21 | const userCredits = await getUserCredits(); 22 | 23 | if (!user) { 24 | return ( 25 |
26 |
27 | 28 | 29 | RoadmapAI 30 | 31 | 32 |
33 | 34 | 35 | 36 |
37 |
38 |
39 | ); 40 | } 41 | 42 | return ( 43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | RoadmapAI 51 | 52 | 53 | 54 |
55 |
56 |
57 | 58 | 59 | 60 | 0 64 | ? "outline" 65 | : "destructive" 66 | } 67 | > 68 | {userCredits} 69 | 70 | 71 | Credits Remaining 72 | 73 | 74 |
75 |
76 |
77 | 78 | 79 | 80 |
81 |
82 | ); 83 | } 84 | 85 | export default AppBar; 86 | -------------------------------------------------------------------------------- /src/components/app/mobile-drawer.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Sheet, 3 | SheetClose, 4 | SheetContent, 5 | SheetTrigger, 6 | } from "@/components/ui/sheet"; 7 | import { Menu } from "lucide-react"; 8 | import Link from "next/link"; 9 | 10 | const MobileDrawer = () => { 11 | return ( 12 | 13 | 14 | 15 | 16 | 20 | 21 | Explore 22 | 23 | 24 | Dashboard 25 | 26 | 27 | Roadmap 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default MobileDrawer; 35 | -------------------------------------------------------------------------------- /src/components/app/nav-items.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { Link } from "next-view-transitions"; 5 | import { usePathname } from "next/navigation"; 6 | 7 | export default function NavItems() { 8 | const pathname = usePathname(); 9 | return ( 10 |
11 | 15 | 25 | 26 | 30 | 40 | 41 | 45 | 55 | 56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/components/app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { GoogleAnalytics } from "@next/third-parties/google"; 4 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 5 | import * as React from "react"; 6 | import { Toaster } from "sonner"; 7 | import { ClerkProvider } from "@clerk/nextjs"; 8 | import { neobrutalism } from "@clerk/themes"; 9 | import { ViewTransitions } from "next-view-transitions"; 10 | 11 | const queryClient = new QueryClient(); 12 | 13 | function Providers({ children }: { children: React.ReactNode }) { 14 | return ( 15 | 16 | 24 | 25 | 26 | {children} 27 | 28 | {/* */} 29 | 30 | 31 | 32 | ); 33 | } 34 | 35 | export default Providers; 36 | -------------------------------------------------------------------------------- /src/components/flow-components/Flow.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Roadmap from "@/components/roadmap/roadmap"; 3 | import { Suspense } from "react"; 4 | import { ReactFlowProvider } from "reactflow"; 5 | 6 | export const Flow = ({ roadmapId }: { roadmapId: string }) => { 7 | return ( 8 | }> 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/flow-components/Instructions.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "@/components/ui/card"; 2 | import { Network } from "lucide-react"; 3 | import Link from "next/link"; 4 | 5 | function RoadmapEmptyState() { 6 | return ( 7 |
8 | 9 | 10 | Generate a new roadmap 11 | 12 |
13 | ); 14 | } 15 | 16 | const AppInstructions = () => { 17 | return ( 18 |
19 | 20 |

21 | Some Instructions before you proceed 22 |

23 |
    24 |
  1. 25 | Use Cohere from the dropdown to generate free roadmaps while you 26 | have credits. 27 |
  2. 28 |
  3. If an error occurs, please try again a few times.
  4. 29 |
  5. 30 | Look out for previously generated roadmaps in the{" "} 31 | 32 | Explorer 33 | {" "} 34 | section. 35 |
  6. 36 |
  7. If you finish your credits, please add your own api key.
  8. 37 |
  9. 38 | Avoid searching for NSFW/sensitive content. The roadmap for it wont 39 | be generated. 40 |
  10. 41 |
  11. 42 | If you like this tool, please give it a star on{" "} 43 | 47 | GitHub 48 | 49 | . 50 |
  12. 51 |
52 |
53 |
54 | ); 55 | }; 56 | 57 | const Instructions = () => { 58 | return ( 59 |
60 |
61 | 62 |
63 |
64 | ); 65 | }; 66 | 67 | export default Instructions; 68 | -------------------------------------------------------------------------------- /src/components/flow-components/Search.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { getPaginatedPublicRoadmaps } from "@/actions/roadmaps"; 3 | import { timeFromNow } from "@/lib/utils"; 4 | import { zodResolver } from "@hookform/resolvers/zod"; 5 | import { useQuery } from "@tanstack/react-query"; 6 | import { Loader2 } from "lucide-react"; 7 | import { useEffect, useState } from "react"; 8 | import { useForm } from "react-hook-form"; 9 | import { z } from "zod"; 10 | import { EmptyAlert } from "../alerts/EmptyAlert"; 11 | import { SearchAlert } from "../alerts/SearchAlert"; 12 | import { Input } from "../ui/input"; 13 | import RoadmapCard from "./roadmap-card"; 14 | import { useSearchParams } from "next/navigation"; 15 | import { Pagination } from "../ui/pagination"; 16 | import ExplorePagination from "./explore-pagination"; 17 | 18 | const formSchema = z.object({ 19 | query: z.string().min(1, { message: "Please enter a query to search" }), 20 | }); 21 | 22 | const Search = () => { 23 | const form = useForm({ 24 | resolver: zodResolver(formSchema), 25 | defaultValues: { 26 | query: "", 27 | }, 28 | }); 29 | 30 | const searchParams = useSearchParams(); 31 | 32 | const page = searchParams.get("page") || "1"; 33 | 34 | const { data: roadmaps, isLoading } = useQuery({ 35 | queryKey: ["public-roadmaps"], 36 | queryFn: async () => { 37 | // TODO : page count and exceed page count logic 38 | const { roadmaps, pageCount } = await getPaginatedPublicRoadmaps({ 39 | page: parseInt(page), 40 | pageSize: 21, 41 | }); 42 | return roadmaps; 43 | }, 44 | }); 45 | 46 | const [filteredRoadmaps, setFilteredRoadmaps] = useState([]); 47 | 48 | useEffect(() => { 49 | if (roadmaps) { 50 | setFilteredRoadmaps(roadmaps); 51 | } 52 | }, [roadmaps]); 53 | 54 | if (roadmaps?.length === 0 && filteredRoadmaps?.length === 0) { 55 | return ( 56 | 57 | ); 58 | } 59 | 60 | return ( 61 |
62 |
63 | { 69 | form.setValue("query", e.target.value); 70 | setFilteredRoadmaps( 71 | roadmaps?.filter((roadmap) => 72 | roadmap.title 73 | .toLowerCase() 74 | .includes(e.target.value.toLowerCase()) 75 | ) 76 | ); 77 | }} 78 | /> 79 |
{" "} 80 | {isLoading ? ( 81 |
82 | 83 |
84 | ) : filteredRoadmaps && filteredRoadmaps?.length > 0 ? ( 85 | <> 86 |
87 | {filteredRoadmaps?.map((roadmap) => ( 88 | 98 | ))} 99 |
100 | 101 | 102 | ) : ( 103 |
104 | 105 |
106 | )} 107 |
108 | ); 109 | }; 110 | 111 | export default Search; 112 | -------------------------------------------------------------------------------- /src/components/flow-components/custom-node.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { memo } from "react"; 4 | import { Handle, Position } from "reactflow"; 5 | 6 | function CustomNode({ data }: { data: any }) { 7 | return ( 8 |
9 |
10 |
11 | {data.emoji} 12 |
13 |
14 |
{data.name}
15 |
{data.job}
16 |
17 |
18 | 19 | 24 | 29 |
30 | ); 31 | } 32 | 33 | export default memo(CustomNode); 34 | -------------------------------------------------------------------------------- /src/components/flow-components/data.js: -------------------------------------------------------------------------------- 1 | export const tempData = [ 2 | { 3 | name: "Introduction to Node.js", 4 | children: [ 5 | { 6 | name: "What is Node.js?", 7 | }, 8 | { 9 | name: "Advantages of Node.js", 10 | }, 11 | { 12 | name: "Setting up Node.js environment", 13 | }, 14 | ], 15 | }, 16 | { 17 | name: "Node.js Basics", 18 | children: [ 19 | { 20 | name: "Working with npm (Node Package Manager)", 21 | }, 22 | { 23 | name: "Basic modules and their usage", 24 | }, 25 | { 26 | name: "Asynchronous programming with callbacks", 27 | }, 28 | ], 29 | }, 30 | { 31 | name: "Node.js Advanced Concepts", 32 | children: [ 33 | { 34 | name: "Event loop and event emitters", 35 | }, 36 | { 37 | name: "Streams and buffers", 38 | }, 39 | { 40 | name: "Error handling and debugging", 41 | }, 42 | ], 43 | }, 44 | { 45 | name: "Node.js Frameworks", 46 | children: [ 47 | { 48 | name: "Express.js framework", 49 | }, 50 | { 51 | name: "Sails.js framework", 52 | }, 53 | { 54 | name: "Koa.js framework", 55 | }, 56 | ], 57 | }, 58 | { 59 | name: "Database Integration with Node.js", 60 | children: [ 61 | { 62 | name: "Connecting to databases", 63 | }, 64 | { 65 | name: "CRUD operations with databases", 66 | }, 67 | { 68 | name: "ORM (Object-Relational Mapping) libraries", 69 | }, 70 | ], 71 | }, 72 | { 73 | name: "Testing and Debugging in Node.js", 74 | children: [ 75 | { 76 | name: "Unit testing with Mocha and Chai", 77 | }, 78 | { 79 | name: "Debugging with Node.js inspector", 80 | }, 81 | { 82 | name: "Performance optimization", 83 | }, 84 | ], 85 | }, 86 | { 87 | name: "Building RESTful APIs with Node.js", 88 | children: [ 89 | { 90 | name: "Understanding REST architecture", 91 | }, 92 | { 93 | name: "Express.js routing for APIs", 94 | }, 95 | { 96 | name: "Authentication and security", 97 | }, 98 | ], 99 | }, 100 | { 101 | name: "Real-time Applications with Node.js", 102 | children: [ 103 | { 104 | name: "Socket.io for real-time communication", 105 | }, 106 | { 107 | name: "Building chat applications", 108 | }, 109 | { 110 | name: "Scalability and load balancing", 111 | }, 112 | ], 113 | }, 114 | { 115 | name: "Deployment and DevOps for Node.js", 116 | children: [ 117 | { 118 | name: "Deploying Node.js applications", 119 | }, 120 | { 121 | name: "Containerization with Docker", 122 | }, 123 | { 124 | name: "DevOps best practices", 125 | }, 126 | ], 127 | }, 128 | ] -------------------------------------------------------------------------------- /src/components/flow-components/expand-collapse.tsx: -------------------------------------------------------------------------------- 1 | import { HierarchyNode, hierarchy } from "d3-hierarchy"; 2 | import { Loader2 } from "lucide-react"; 3 | import { ReactFlowProvider } from "reactflow"; 4 | import "reactflow/dist/base.css"; 5 | import "reactflow/dist/style.css"; 6 | import { Node } from "@/lib/shared/types/common"; 7 | import { Drawer } from "./drawer"; 8 | import ReactFlowPro from "./react-flow-pro"; 9 | 10 | type Props = { 11 | data: Node[]; 12 | isPending: boolean; 13 | roadmapId?: string; 14 | }; 15 | 16 | function ExpandCollapse(props: Props) { 17 | const { data, isPending, roadmapId } = props; 18 | 19 | const h: HierarchyNode = hierarchy(data[0]); 20 | h.descendants().forEach((d: any, i: number) => { 21 | d.id = `${i}`; 22 | d._children = d.children; 23 | d.children = null; 24 | }); 25 | 26 | if (isPending) 27 | return ( 28 |
29 | 30 |
31 | ); 32 | 33 | return ( 34 |
35 | 36 | 37 | 38 | 39 |
40 | ); 41 | } 42 | 43 | export default ExpandCollapse; 44 | -------------------------------------------------------------------------------- /src/components/flow-components/explore-pagination.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Pagination, 5 | PaginationContent, 6 | PaginationEllipsis, 7 | PaginationItem, 8 | PaginationLink, 9 | PaginationNext, 10 | PaginationPrevious, 11 | } from "@/components/ui/pagination"; 12 | import { useSearchParams } from "next/navigation"; 13 | 14 | export default function ExplorePagination() { 15 | const searchParams = useSearchParams(); 16 | const page = searchParams.get("page") || "1"; 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | {page} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/flow-components/generate-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Button } from "@/components/ui/button"; 3 | import { LoaderCircle, Wand } from "lucide-react"; 4 | 5 | interface Props { 6 | onClick: ( 7 | e: 8 | | React.MouseEvent 9 | | React.FormEvent 10 | | React.KeyboardEvent 11 | ) => Promise; 12 | disabled: boolean; 13 | } 14 | 15 | const GenerateButton = ({ disabled, onClick }: Props) => { 16 | return ( 17 | 30 | ); 31 | }; 32 | 33 | export default GenerateButton; 34 | -------------------------------------------------------------------------------- /src/components/flow-components/model-select.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Select, 3 | SelectContent, 4 | SelectItem, 5 | SelectTrigger, 6 | SelectValue, 7 | } from "@/components/ui/select"; 8 | import { useEffect } from "react"; 9 | import { useShallow } from "zustand/react/shallow"; 10 | import { availableModels, modelKeys } from "@/lib/shared/constants"; 11 | import { UModel, useUIStore } from "@/lib/stores"; 12 | 13 | interface ModalSelectProps { 14 | disabled: boolean; 15 | } 16 | 17 | const ModelSelect = ({ disabled }: ModalSelectProps) => { 18 | const { setModel, model } = useUIStore( 19 | useShallow((state) => ({ 20 | setModel: state.setModel, 21 | model: state.model, 22 | })), 23 | ); 24 | const onValueChange = (val: string) => setModel(val as UModel); 25 | 26 | useEffect(() => { 27 | const modelKey = localStorage.getItem("model"); 28 | const exist = modelKeys.find((key) => key === modelKey); 29 | if (exist && modelKey) { 30 | setModel(modelKey as UModel); 31 | } 32 | }, []); 33 | 34 | useEffect(() => { 35 | localStorage.setItem("model", model); 36 | }, [model]); 37 | 38 | return ( 39 | 51 | ); 52 | }; 53 | 54 | export default ModelSelect; 55 | -------------------------------------------------------------------------------- /src/components/landing/roadmap-confetti.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | import confetti, { Options } from "canvas-confetti"; 5 | 6 | export default function Confetti() { 7 | useEffect(() => { 8 | const duration = 15 * 1000; 9 | const animationEnd = Date.now() + duration; 10 | const defaults: Options = { 11 | startVelocity: 30, 12 | spread: 360, 13 | ticks: 60, 14 | zIndex: 0, 15 | }; 16 | 17 | function randomInRange(min: number, max: number): number { 18 | return Math.random() * (max - min) + min; 19 | } 20 | 21 | const interval: ReturnType = setInterval(function () { 22 | const timeLeft = animationEnd - Date.now(); 23 | 24 | if (timeLeft <= 0) { 25 | return clearInterval(interval); 26 | } 27 | 28 | const particleCount = 50 * (timeLeft / duration); 29 | 30 | confetti({ 31 | ...defaults, 32 | particleCount, 33 | origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 }, 34 | }); 35 | 36 | confetti({ 37 | ...defaults, 38 | particleCount, 39 | origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 }, 40 | }); 41 | }, 250); 42 | 43 | return () => clearInterval(interval); 44 | }, []); 45 | 46 | return null; 47 | } 48 | -------------------------------------------------------------------------------- /src/components/landing/roadmap-features.tsx: -------------------------------------------------------------------------------- 1 | import { Book, GlobeLock, Network } from "lucide-react"; 2 | 3 | const features = [ 4 | { 5 | name: "Visualise Roadmap Tree.", 6 | description: 7 | "We have built a visualisation tool to help you understand the roadmap better.", 8 | icon: Network, 9 | }, 10 | { 11 | name: "Book Recomendations.", 12 | description: 13 | "We help you with relivant books to read to understand the topic better. Powered by Oriley.", 14 | icon: Book, 15 | }, 16 | { 17 | name: "Privacy Friendly.", 18 | description: 19 | "If you want to generate a roadmap without sharing it to the world, we got you covered.", 20 | icon: GlobeLock, 21 | }, 22 | ]; 23 | 24 | export default function RoadmapFeatures() { 25 | return ( 26 |
27 |
28 |
29 |

30 | Everything you need 31 |

32 |

33 | No structured learning plan? No problem. 34 |

35 |

36 | We help you generate a structured learning roadmap for any topic you 37 | want to learn. Just enter the topic and let us handle the rest. 38 |

39 |
40 |
41 |
42 |
43 | App screenshot 50 | 54 |
55 |
56 |
57 | {features.map((feature) => ( 58 |
59 |
60 |
{" "} 66 |
{feature.description}
67 |
68 | ))} 69 |
70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/components/landing/roadmap-icon-cloud.tsx: -------------------------------------------------------------------------------- 1 | import IconCloud from "@/components/magicui/icon-cloud"; 2 | 3 | const slugs = [ 4 | "typescript", 5 | "javascript", 6 | "dart", 7 | "java", 8 | "react", 9 | "flutter", 10 | "android", 11 | "html5", 12 | "css3", 13 | "nodedotjs", 14 | "express", 15 | "nextdotjs", 16 | "prisma", 17 | "amazonaws", 18 | "postgresql", 19 | "firebase", 20 | "nginx", 21 | "vercel", 22 | "testinglibrary", 23 | "jest", 24 | "cypress", 25 | "docker", 26 | "git", 27 | "jira", 28 | "github", 29 | "gitlab", 30 | "visualstudiocode", 31 | "androidstudio", 32 | "sonarqube", 33 | "figma", 34 | ]; 35 | 36 | export function RoadmapIconCloud() { 37 | return ( 38 |
39 | 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/components/landing/roadmap-stats.tsx: -------------------------------------------------------------------------------- 1 | const stats = [ 2 | { id: 1, name: "Roadmaps Generated", value: "250+" }, 3 | { id: 2, name: "Unique Visitors", value: "5.64k" }, 4 | { id: 3, name: "Roadmaps Shared", value: "100+" }, 5 | ]; 6 | 7 | export default function RoadmapStats() { 8 | return ( 9 |
10 |
11 |
12 | {stats.map((stat) => ( 13 |
17 |
{stat.name}
18 |
19 | {stat.value} 20 |
21 |
22 | ))} 23 |
24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/landing/roadmap-team.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | type People = { 4 | name: string; 5 | role: string; 6 | imageUrl: string; 7 | link: string; 8 | }; 9 | 10 | const people: People[] = [ 11 | { 12 | name: "Vishwajeet Raj", 13 | role: "Software Engineer", 14 | imageUrl: 15 | "https://dqy38fnwh4fqs.cloudfront.net/UH8OABPQAKN8DPE29GOMBJBMQBJB/h8oabpqakn8dpe29gombjbmqbjb-profile.webp", 16 | link: "https://peerlist.io/vishwajeetraj11", 17 | }, 18 | { 19 | name: "Milind Mishra", 20 | role: "Software Engineer", 21 | imageUrl: 22 | "https://dqy38fnwh4fqs.cloudfront.net/UHDNGKG7BMJ8PKPFNLPRDDRNOAG7/9922bb83-bd73-429b-bb25-4cb381f51a4a.png", 23 | link: "https://peerlist.io/milind", 24 | }, 25 | { 26 | name: "Sinchan Dasgupta", 27 | role: "Software Engineer", 28 | imageUrl: 29 | "https://dqy38fnwh4fqs.cloudfront.net/UH6AJJ8JJJMLE771G6KNQDLKP68R/h6ajj8jjjmle771g6knqdlkp68r-profile.webp", 30 | link: "https://peerlist.io/syndg", 31 | }, 32 | { 33 | name: "Suyash Patil", 34 | role: "Software Engineer", 35 | imageUrl: 36 | "https://dqy38fnwh4fqs.cloudfront.net/UHA9NNDRJ9PBRGD1JGBGJ9MP8A7D/ha9nndrj9pbrgd1jgbgj9mp8a7d-profile", 37 | link: "https://peerlist.io/suyashpatil", 38 | }, 39 | ]; 40 | 41 | export default function RoadmapTeam() { 42 | return ( 43 |
44 |
45 |
46 |

47 | Meet{" "} 48 | 49 | 55 | 56 | Makkhan Labs 57 | 58 | 59 |

60 |

61 | It started from Vishwajeet generating roadmap tree from the AI and 62 | rendering React flow, rest of us liked the idea and built the bells 63 | and whistles around it. 64 |

65 |
66 |
    70 | {people.map((person) => ( 71 |
  • 72 |
    73 | 78 |
    79 |

    80 | {person.name} 81 |

    82 |

    83 | {person.role} 84 |

    85 | 94 |
    95 |
    96 |
  • 97 | ))} 98 |
99 |
100 |
101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /src/components/magicui/dot-pattern.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { useId } from "react"; 3 | 4 | interface DotPatternProps { 5 | width?: any; 6 | height?: any; 7 | x?: any; 8 | y?: any; 9 | cx?: any; 10 | cy?: any; 11 | cr?: any; 12 | className?: string; 13 | [key: string]: any; 14 | } 15 | export function DotPattern({ 16 | width = 16, 17 | height = 16, 18 | x = 0, 19 | y = 0, 20 | cx = 1, 21 | cy = 1, 22 | cr = 1, 23 | className, 24 | ...props 25 | }: DotPatternProps) { 26 | const id = useId(); 27 | 28 | return ( 29 | 52 | ); 53 | } 54 | 55 | export default DotPattern; 56 | -------------------------------------------------------------------------------- /src/components/magicui/icon-cloud.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | import { useEffect, useMemo, useState } from "react"; 5 | import { 6 | Cloud, 7 | fetchSimpleIcons, 8 | ICloud, 9 | renderSimpleIcon, 10 | SimpleIcon, 11 | } from "react-icon-cloud"; 12 | 13 | export const cloudProps: Omit = { 14 | containerProps: { 15 | style: { 16 | display: "flex", 17 | justifyContent: "center", 18 | alignItems: "center", 19 | width: "100%", 20 | paddingTop: 40, 21 | }, 22 | }, 23 | options: { 24 | reverse: true, 25 | depth: 1, 26 | wheelZoom: false, 27 | imageScale: 2, 28 | activeCursor: "default", 29 | tooltip: "native", 30 | initial: [0.1, -0.1], 31 | clickToFront: 500, 32 | tooltipDelay: 0, 33 | outlineColour: "#0000", 34 | maxSpeed: 0.04, 35 | minSpeed: 0.02, 36 | // dragControl: false, 37 | }, 38 | }; 39 | 40 | export const renderCustomIcon = (icon: SimpleIcon, theme: string) => { 41 | const bgHex = theme === "light" ? "#f3f2ef" : "#080510"; 42 | const fallbackHex = theme === "light" ? "#6e6e73" : "#ffffff"; 43 | const minContrastRatio = theme === "dark" ? 2 : 1.2; 44 | 45 | return renderSimpleIcon({ 46 | icon, 47 | bgHex, 48 | fallbackHex, 49 | minContrastRatio, 50 | size: 42, 51 | aProps: { 52 | href: undefined, 53 | target: undefined, 54 | rel: undefined, 55 | onClick: (e: any) => e.preventDefault(), 56 | }, 57 | }); 58 | }; 59 | 60 | export type DynamicCloudProps = { 61 | iconSlugs: string[]; 62 | }; 63 | 64 | type IconData = Awaited>; 65 | 66 | export default function IconCloud({ iconSlugs }: DynamicCloudProps) { 67 | const [data, setData] = useState(null); 68 | const { theme } = useTheme(); 69 | 70 | useEffect(() => { 71 | fetchSimpleIcons({ slugs: iconSlugs }).then(setData); 72 | }, [iconSlugs]); 73 | 74 | const renderedIcons = useMemo(() => { 75 | if (!data) return null; 76 | 77 | return Object.values(data.simpleIcons).map((icon) => 78 | renderCustomIcon(icon, theme || "light") 79 | ); 80 | }, [data, theme]); 81 | 82 | return ( 83 | // @ts-ignore 84 | 85 | <>{renderedIcons} 86 | 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /src/components/marketing/banner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from "next/link"; 3 | import { XMarkIcon } from "@heroicons/react/20/solid"; 4 | import { usePathname } from "next/navigation"; 5 | import { useEffect, useState } from "react"; 6 | import { useHasMounted } from "@/hooks/use-has-mounted"; 7 | 8 | function PeerlistBanner() { 9 | const hasMounted = useHasMounted(); 10 | const [isHidden, setIsHidden] = useState(false); 11 | 12 | useEffect(() => { 13 | const value = localStorage.getItem("banner_is_hidden"); 14 | setIsHidden(value === "true"); 15 | }, []); 16 | 17 | const handleDismiss = () => { 18 | setIsHidden(true); 19 | localStorage.setItem("banner_is_hidden", "true"); 20 | }; 21 | 22 | if (!hasMounted || isHidden) { 23 | return null; 24 | } 25 | 26 | return ( 27 |
28 |

29 | 33 | 34 | WE ARE THE PRODUCT OF THE MONTH 🏆 THANK YOU! 35 | 36 | 37 |

38 |
39 | 47 |
48 |
49 | ); 50 | } 51 | 52 | export default function Banner() { 53 | const pathname = usePathname(); 54 | 55 | if (pathname !== "/") { 56 | return null; 57 | } 58 | 59 | return ; 60 | } 61 | -------------------------------------------------------------------------------- /src/components/marketing/cta.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import NeubrutalismButton from "@/components/ui/neobrutalism-button"; 3 | 4 | export default function CTA() { 5 | return ( 6 |
7 |
8 |
9 |
10 |
11 |

12 | 13 | We're open closed 14 | source! 15 | 16 |

17 |

18 | We're working on a new version of our platform, expect new 19 | features and improvements soon! 20 |

21 |
22 | 23 | 24 | 25 | Community 26 | 27 | 28 | 29 | 30 | 31 | 32 | Updates 33 | 34 | 35 | 36 |
37 |
38 |
39 |
40 | Repository Screenshot 45 |
46 |
47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/components/marketing/text-ticker.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useRef } from "react"; 4 | import { useInView, useMotionValue, useSpring } from "framer-motion"; 5 | 6 | export default function TextTicker({ 7 | value, 8 | direction = "up", 9 | }: { 10 | value: number; 11 | direction?: "up" | "down"; 12 | }) { 13 | const ref = useRef(null); 14 | const motionValue = useMotionValue(direction === "down" ? value : 0); 15 | const springValue = useSpring(motionValue, { 16 | damping: 100, 17 | stiffness: 100, 18 | }); 19 | const isInView = useInView(ref, { once: true, margin: "-100px" }); 20 | 21 | useEffect(() => { 22 | if (isInView) { 23 | motionValue.set(direction === "down" ? 0 : value); 24 | } 25 | }, [motionValue, isInView]); 26 | 27 | useEffect( 28 | () => 29 | springValue.on("change", (latest) => { 30 | if (ref.current) { 31 | ref.current.textContent = Intl.NumberFormat("en-US").format( 32 | latest.toFixed(0), 33 | ); 34 | } 35 | }), 36 | [springValue], 37 | ); 38 | 39 | return ; 40 | } 41 | -------------------------------------------------------------------------------- /src/components/roadmap/code-viewer.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogHeader, 7 | DialogTitle, 8 | DialogTrigger, 9 | } from "@/components/ui/dialog"; 10 | 11 | export function CodeViewer() { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | View code 20 | 21 | You can use the following code to start integrating your current 22 | prompt and settings into your application. 23 | 24 | 25 |
26 |
27 |
28 |               
29 |                 
30 |                   import os
31 |                 
32 |                 
33 |                   import openai
34 |                 
35 |                 
36 |                 
37 |                   openai.api_key = os.getenv(
38 |                   
39 |                     "OPENAI_API_KEY"
40 |                   
41 |                   )
42 |                 
43 |                 
44 |                 response = openai.Completion.create(
45 |                 
46 |                   {" "}
47 |                   model=
48 |                   "davinci",
49 |                 
50 |                 
51 |                   {" "}
52 |                   prompt="",
53 |                 
54 |                 
55 |                   {" "}
56 |                   temperature=0.9,
57 |                 
58 |                 
59 |                   {" "}
60 |                   max_tokens=5,
61 |                 
62 |                 
63 |                   {" "}
64 |                   top_p=1,
65 |                 
66 |                 
67 |                   {" "}
68 |                   frequency_penalty=0,
69 |                 
70 |                 
71 |                   {" "}
72 |                   presence_penalty=0,
73 |                 
74 |                 )
75 |               
76 |             
77 |
78 |
79 |

80 | Your API Key can be found here. You should use environment 81 | variables or a secret management tool to expose your key to your 82 | applications. 83 |

84 |
85 |
86 |
87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /src/components/roadmap/data/models.ts: -------------------------------------------------------------------------------- 1 | export const types = ["GPT-3", "Codex"] as const 2 | 3 | export type ModelType = (typeof types)[number] 4 | 5 | export interface Model { 6 | id: string 7 | name: string 8 | description: string 9 | strengths?: string 10 | type: Type 11 | } 12 | 13 | export const models: Model[] = [ 14 | { 15 | id: "c305f976-8e38-42b1-9fb7-d21b2e34f0da", 16 | name: "text-davinci-003", 17 | description: 18 | "Most capable GPT-3 model. Can do any task the other models can do, often with higher quality, longer output and better instruction-following. Also supports inserting completions within text.", 19 | type: "GPT-3", 20 | strengths: 21 | "Complex intent, cause and effect, creative generation, search, summarization for audience", 22 | }, 23 | { 24 | id: "464a47c3-7ab5-44d7-b669-f9cb5a9e8465", 25 | name: "text-curie-001", 26 | description: "Very capable, but faster and lower cost than Davinci.", 27 | type: "GPT-3", 28 | strengths: 29 | "Language translation, complex classification, sentiment, summarization", 30 | }, 31 | { 32 | id: "ac0797b0-7e31-43b6-a494-da7e2ab43445", 33 | name: "text-babbage-001", 34 | description: "Capable of straightforward tasks, very fast, and lower cost.", 35 | type: "GPT-3", 36 | strengths: "Moderate classification, semantic search", 37 | }, 38 | { 39 | id: "be638fb1-973b-4471-a49c-290325085802", 40 | name: "text-ada-001", 41 | description: 42 | "Capable of very simple tasks, usually the fastest model in the GPT-3 series, and lowest cost.", 43 | type: "GPT-3", 44 | strengths: 45 | "Parsing text, simple classification, address correction, keywords", 46 | }, 47 | { 48 | id: "b43c0ea9-5ad4-456a-ae29-26cd77b6d0fb", 49 | name: "code-davinci-002", 50 | description: 51 | "Most capable Codex model. Particularly good at translating natural language to code. In addition to completing code, also supports inserting completions within code.", 52 | type: "Codex", 53 | }, 54 | { 55 | id: "bbd57291-4622-4a21-9eed-dd6bd786fdd1", 56 | name: "code-cushman-001", 57 | description: 58 | "Almost as capable as Davinci Codex, but slightly faster. This speed advantage may make it preferable for real-time applications.", 59 | type: "Codex", 60 | strengths: "Real-time application where low-latency is preferable", 61 | }, 62 | ] 63 | -------------------------------------------------------------------------------- /src/components/roadmap/data/presets.ts: -------------------------------------------------------------------------------- 1 | export interface Preset { 2 | id: string 3 | name: string 4 | } 5 | 6 | export const presets: Preset[] = [ 7 | { 8 | id: "9cb0e66a-9937-465d-a188-2c4c4ae2401f", 9 | name: "Grammatical Standard English", 10 | }, 11 | { 12 | id: "61eb0e32-2391-4cd3-adc3-66efe09bc0b7", 13 | name: "Summarize for a 2nd grader", 14 | }, 15 | { 16 | id: "a4e1fa51-f4ce-4e45-892c-224030a00bdd", 17 | name: "Text to command", 18 | }, 19 | { 20 | id: "cc198b13-4933-43aa-977e-dcd95fa30770", 21 | name: "Q&A", 22 | }, 23 | { 24 | id: "adfa95be-a575-45fd-a9ef-ea45386c64de", 25 | name: "English to other languages", 26 | }, 27 | { 28 | id: "c569a06a-0bd6-43a7-adf9-bf68c09e7a79", 29 | name: "Parse unstructured data", 30 | }, 31 | { 32 | id: "15ccc0d7-f37a-4f0a-8163-a37e162877dc", 33 | name: "Classification", 34 | }, 35 | { 36 | id: "4641ef41-1c0f-421d-b4b2-70fe431081f3", 37 | name: "Natural language to Python", 38 | }, 39 | { 40 | id: "48d34082-72f3-4a1b-a14d-f15aca4f57a0", 41 | name: "Explain code", 42 | }, 43 | { 44 | id: "dfd42fd5-0394-4810-92c6-cc907d3bfd1a", 45 | name: "Chat", 46 | }, 47 | ] 48 | -------------------------------------------------------------------------------- /src/components/roadmap/maxlength-selector.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { SliderProps } from "@radix-ui/react-slider"; 5 | 6 | import { 7 | HoverCard, 8 | HoverCardContent, 9 | HoverCardTrigger, 10 | } from "@/components/ui/hover-card"; 11 | import { Label } from "@/components/ui/label"; 12 | import { Slider } from "@/components/ui/slider"; 13 | 14 | interface MaxLengthSelectorProps { 15 | defaultValue: SliderProps["defaultValue"]; 16 | } 17 | 18 | export function MaxLengthSelector({ defaultValue }: MaxLengthSelectorProps) { 19 | const [value, setValue] = React.useState(defaultValue); 20 | 21 | return ( 22 |
23 | 24 | 25 |
26 |
27 | 28 | 29 | {value} 30 | 31 |
32 | 41 |
42 |
43 | 48 | The maximum number of tokens to generate. Requests can use up to 2,048 49 | or 4,000 tokens, shared between prompt and completion. The exact limit 50 | varies by model. 51 | 52 |
53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/components/roadmap/preset-save.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogFooter, 7 | DialogHeader, 8 | DialogTitle, 9 | DialogTrigger, 10 | } from "@/components/ui/dialog"; 11 | import { Input } from "@/components/ui/input"; 12 | import { Label } from "@/components/ui/label"; 13 | import { Wand } from "lucide-react"; 14 | 15 | export function PresetSave() { 16 | return ( 17 | 18 | 19 | 23 | 24 | 25 | 26 | Save preset 27 | 28 | This will save the current playground state as a preset which you 29 | can access later or share with others. 30 | 31 | 32 |
33 |
34 | 35 | 36 |
37 |
38 | 39 | 40 |
41 |
42 | 43 | 44 | 45 |
46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/components/roadmap/preset-selector.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { useRouter } from "next/navigation"; 5 | import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; 6 | import { PopoverProps } from "@radix-ui/react-popover"; 7 | 8 | import { cn } from "@/lib/utils"; 9 | import { Button } from "@/components/ui/button"; 10 | import { 11 | Command, 12 | CommandEmpty, 13 | CommandGroup, 14 | CommandInput, 15 | CommandItem, 16 | } from "@/components/ui/command"; 17 | import { 18 | Popover, 19 | PopoverContent, 20 | PopoverTrigger, 21 | } from "@/components/ui/popover"; 22 | 23 | import { Preset } from "./data/presets"; 24 | 25 | interface PresetSelectorProps extends PopoverProps { 26 | presets: Preset[]; 27 | } 28 | 29 | export function PresetSelector({ presets, ...props }: PresetSelectorProps) { 30 | const [open, setOpen] = React.useState(false); 31 | const [selectedPreset, setSelectedPreset] = React.useState(); 32 | const router = useRouter(); 33 | 34 | return ( 35 | 36 | 37 | 47 | 48 | 49 | 50 | 51 | No presets found. 52 | 53 | {presets.map((preset) => ( 54 | { 57 | setSelectedPreset(preset); 58 | setOpen(false); 59 | }} 60 | > 61 | {preset.name} 62 | 70 | 71 | ))} 72 | 73 | 74 | {/* router.push("/examples")}> 75 | More examples 76 | */} 77 | 78 | 79 | 80 | 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /src/components/roadmap/preset-share.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CopyIcon } from "@radix-ui/react-icons"; 4 | 5 | import { Button } from "@/components/ui/button"; 6 | import { Input } from "@/components/ui/input"; 7 | import { Label } from "@/components/ui/label"; 8 | import { 9 | Dialog, 10 | DialogContent, 11 | DialogDescription, 12 | DialogHeader, 13 | DialogTitle, 14 | DialogTrigger, 15 | } from "@/components/ui/dialog"; 16 | import { QRCodeSVG } from "qrcode.react"; 17 | import { toast } from "sonner"; 18 | import { usePathname } from "next/navigation"; 19 | 20 | export function PresetShare() { 21 | const pathName = usePathname(); 22 | 23 | return ( 24 | 25 | 26 | 29 | 30 | 31 | 32 | Share roadmap 33 | 34 | Anyone who has this link can view your roadmap. 35 | 36 | 37 |
38 |
39 | 42 | 52 |
53 | 70 |
71 | 76 |
77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /src/components/roadmap/roadmap.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { getRoadmapById } from "@/actions/roadmaps"; 4 | import ExpandCollapse from "@/components/flow-components/expand-collapse"; 5 | import { Separator } from "@/components/ui/separator"; 6 | import { useGenerateRoadmap } from "@/lib/queries"; 7 | import { decodeFromURL } from "@/lib/utils"; 8 | import { useQuery } from "@tanstack/react-query"; 9 | import { Loader2 } from "lucide-react"; 10 | import { useSearchParams } from "next/navigation"; 11 | import { useShallow } from "zustand/react/shallow"; 12 | import { GeneratorControls } from "@/components/flow-components/generator-controls"; 13 | import { useUIStore } from "../../lib/stores/useUI"; 14 | import Instructions from "@/components/flow-components/Instructions"; 15 | 16 | interface Props { 17 | roadmapId?: string; 18 | } 19 | 20 | export default function Roadmap({ roadmapId }: Props) { 21 | const { model, modelApiKey, query } = useUIStore( 22 | useShallow((state) => ({ 23 | model: state.model, 24 | query: state.query, 25 | modelApiKey: state.modelApiKey, 26 | })), 27 | ); 28 | 29 | const { data: roadmap, isPending: isRoadmapPending } = useQuery({ 30 | queryFn: async () => { 31 | let roadmap = await getRoadmapById(roadmapId || ""); 32 | if (roadmap) { 33 | let json = JSON.parse(roadmap.content); 34 | roadmap.content = json; 35 | return roadmap; 36 | } 37 | throw Error("error"); 38 | }, 39 | queryKey: ["Roadmap", roadmapId], 40 | enabled: Boolean(roadmapId), 41 | }); 42 | 43 | const { data, mutate, isPending } = useGenerateRoadmap( 44 | query, 45 | model, 46 | modelApiKey, 47 | ); 48 | 49 | const params = useSearchParams(); 50 | 51 | const renderFlow = 52 | roadmap?.content[0] || 53 | data?.tree?.[0]?.name || 54 | decodeFromURL(params)?.[0]?.name; 55 | 56 | return ( 57 | <> 58 |
59 | 69 |
70 | 71 | {isPending ? ( 72 |
73 | 74 |
75 | ) : isRoadmapPending && roadmapId ? ( 76 |
77 |
78 | 79 |
80 |
81 | ) : ( 82 | <> 83 | {renderFlow ? ( 84 | 90 | ) : ( 91 | 92 | )} 93 | 94 | )} 95 | 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /src/components/roadmap/temperature-selector.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { SliderProps } from "@radix-ui/react-slider"; 5 | 6 | import { 7 | HoverCard, 8 | HoverCardContent, 9 | HoverCardTrigger, 10 | } from "@/components/ui/hover-card"; 11 | import { Label } from "@/components/ui/label"; 12 | import { Slider } from "@/components/ui/slider"; 13 | 14 | interface TemperatureSelectorProps { 15 | defaultValue: SliderProps["defaultValue"]; 16 | } 17 | 18 | export function TemperatureSelector({ 19 | defaultValue, 20 | }: TemperatureSelectorProps) { 21 | const [value, setValue] = React.useState(defaultValue); 22 | 23 | return ( 24 |
25 | 26 | 27 |
28 |
29 | 30 | 31 | {value} 32 | 33 |
34 | 43 |
44 |
45 | 50 | Controls randomness: lowering results in less random completions. As 51 | the temperature approaches zero, the model will become deterministic 52 | and repetitive. 53 | 54 |
55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/components/roadmap/top-p-selector.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { SliderProps } from "@radix-ui/react-slider"; 5 | 6 | import { 7 | HoverCard, 8 | HoverCardContent, 9 | HoverCardTrigger, 10 | } from "@/components/ui/hover-card"; 11 | import { Label } from "@/components/ui/label"; 12 | import { Slider } from "@/components/ui/slider"; 13 | 14 | interface TopPSelectorProps { 15 | defaultValue: SliderProps["defaultValue"]; 16 | } 17 | 18 | export function TopPSelector({ defaultValue }: TopPSelectorProps) { 19 | const [value, setValue] = React.useState(defaultValue); 20 | 21 | return ( 22 |
23 | 24 | 25 |
26 |
27 | 28 | 29 | {value} 30 | 31 |
32 | 41 |
42 |
43 | 48 | Control diversity via nucleus sampling: 0.5 means half of all 49 | likelihood-weighted options are considered. 50 | 51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/components/testimonials/roadmap-testimonial.tsx: -------------------------------------------------------------------------------- 1 | import { StarIcon } from "@heroicons/react/20/solid"; 2 | 3 | export default function RoadmapTestimonial() { 4 | return ( 5 |
6 |
7 | {/*

5 out of 5 stars

8 |
9 |
*/} 15 |
16 |

17 | “100% AGREE! Also, the way team has launched the project is 18 | commendable. This is by far the most successful project launch on 19 | Peerlist” 20 |

21 |
22 |
23 | Akash Bhadange, CEO of Peerlist 28 |
29 |
Akash Bhadange
30 |
CEO of Peerlist
31 |
32 |
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { ChevronDownIcon } from "@radix-ui/react-icons" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Accordion = AccordionPrimitive.Root 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )) 21 | AccordionItem.displayName = "AccordionItem" 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )) 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 52 |
{children}
53 |
54 | )) 55 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 56 | 57 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 58 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )) 33 | Alert.displayName = "Alert" 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )) 45 | AlertTitle.displayName = "AlertTitle" 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | AlertDescription.displayName = "AlertDescription" 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root 6 | 7 | export { AspectRatio } 8 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /src/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons" 3 | import { Slot } from "@radix-ui/react-slot" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Breadcrumb = React.forwardRef< 8 | HTMLElement, 9 | React.ComponentPropsWithoutRef<"nav"> & { 10 | separator?: React.ReactNode 11 | } 12 | >(({ ...props }, ref) =>