Yvonne Priscilla commited on
Commit ·
e6f1924
1
Parent(s): 36f1e82
init commit
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .dockerignore +4 -0
- .eslintrc.json +12 -0
- .gitignore +45 -0
- .prettierrc.json +6 -0
- Dockerfile +18 -0
- components.json +21 -0
- eslint.config.mjs +16 -0
- next.config.ts +8 -0
- package-lock.json +0 -0
- package.json +79 -0
- postcss.config.mjs +5 -0
- prisma.config.ts +13 -0
- prisma/schema.prisma +136 -0
- public/avatar.png +0 -0
- public/book.svg +6 -0
- public/chat.svg +3 -0
- public/dashboard/Background.png +0 -0
- public/dashboard/Background.webp +0 -0
- public/dashboard/logo.png +0 -0
- public/description.svg +3 -0
- public/doc.svg +3 -0
- public/duration.svg +3 -0
- public/file.svg +1 -0
- public/globe.svg +1 -0
- public/loading1.png +0 -0
- public/loading2.png +0 -0
- public/logo.png +0 -0
- public/logo.svg +18 -0
- public/next.svg +1 -0
- public/notifications.svg +3 -0
- public/paragraph.svg +3 -0
- public/robot-1.png +0 -0
- public/vercel.svg +1 -0
- public/video.svg +4 -0
- public/window.svg +1 -0
- src/app/api/agentic/create_weight/route.ts +24 -0
- src/app/api/cv-profile/options/route.ts +52 -0
- src/app/api/cv-profile/route.ts +198 -0
- src/app/api/me/route.ts +22 -0
- src/app/favicon.ico +0 -0
- src/app/globals.css +185 -0
- src/app/layout.tsx +42 -0
- src/app/learning/layout.tsx +109 -0
- src/app/learning/page.tsx +103 -0
- src/app/onboarding/layout.tsx +46 -0
- src/app/onboarding/learningPath.tsx +206 -0
- src/app/onboarding/loading.tsx +38 -0
- src/app/onboarding/page.tsx +19 -0
- src/app/onboarding/page1.tsx +80 -0
- src/app/onboarding/upload.tsx +316 -0
.dockerignore
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
.next
|
| 3 |
+
.env
|
| 4 |
+
.git
|
.eslintrc.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"extends": [
|
| 3 |
+
"next/core-web-vitals",
|
| 4 |
+
"next/typescript"
|
| 5 |
+
],
|
| 6 |
+
"rules": {
|
| 7 |
+
"@typescript-eslint/no-explicit-any": "warn",
|
| 8 |
+
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
| 9 |
+
"react-hooks/exhaustive-deps": "warn",
|
| 10 |
+
"react/display-name": "warn"
|
| 11 |
+
}
|
| 12 |
+
}
|
.gitignore
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.*
|
| 7 |
+
.yarn/*
|
| 8 |
+
!.yarn/patches
|
| 9 |
+
!.yarn/plugins
|
| 10 |
+
!.yarn/releases
|
| 11 |
+
!.yarn/versions
|
| 12 |
+
|
| 13 |
+
# testing
|
| 14 |
+
/coverage
|
| 15 |
+
|
| 16 |
+
# next.js
|
| 17 |
+
/.next/
|
| 18 |
+
/out/
|
| 19 |
+
|
| 20 |
+
# production
|
| 21 |
+
/build
|
| 22 |
+
|
| 23 |
+
# misc
|
| 24 |
+
.DS_Store
|
| 25 |
+
*.pem
|
| 26 |
+
|
| 27 |
+
# debug
|
| 28 |
+
npm-debug.log*
|
| 29 |
+
yarn-debug.log*
|
| 30 |
+
yarn-error.log*
|
| 31 |
+
.pnpm-debug.log*
|
| 32 |
+
|
| 33 |
+
# env files (can opt-in for committing if needed)
|
| 34 |
+
.env*
|
| 35 |
+
|
| 36 |
+
# vercel
|
| 37 |
+
.vercel
|
| 38 |
+
|
| 39 |
+
# typescript
|
| 40 |
+
*.tsbuildinfo
|
| 41 |
+
next-env.d.ts
|
| 42 |
+
|
| 43 |
+
/src/generated/prisma
|
| 44 |
+
|
| 45 |
+
/src/generated/prisma
|
.prettierrc.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"trailingComma": "all",
|
| 3 |
+
"tabWidth": 2,
|
| 4 |
+
"semi": true,
|
| 5 |
+
"singleQuote": false
|
| 6 |
+
}
|
Dockerfile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:18-alpine AS builder
|
| 2 |
+
WORKDIR /app
|
| 3 |
+
COPY package*.json ./
|
| 4 |
+
RUN npm ci
|
| 5 |
+
COPY . .
|
| 6 |
+
RUN npm run build
|
| 7 |
+
|
| 8 |
+
FROM node:18-alpine AS runner
|
| 9 |
+
WORKDIR /app
|
| 10 |
+
ENV NODE_ENV=production
|
| 11 |
+
ENV PORT=7860
|
| 12 |
+
|
| 13 |
+
COPY --from=builder /app/.next/standalone ./
|
| 14 |
+
COPY --from=builder /app/.next/static ./.next/static
|
| 15 |
+
COPY --from=builder /app/public ./public
|
| 16 |
+
|
| 17 |
+
EXPOSE 7860
|
| 18 |
+
CMD ["node", "server.js"]
|
components.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://ui.shadcn.com/schema.json",
|
| 3 |
+
"style": "new-york",
|
| 4 |
+
"rsc": true,
|
| 5 |
+
"tsx": true,
|
| 6 |
+
"tailwind": {
|
| 7 |
+
"config": "",
|
| 8 |
+
"css": "src/app/globals.css",
|
| 9 |
+
"baseColor": "neutral",
|
| 10 |
+
"cssVariables": true,
|
| 11 |
+
"prefix": ""
|
| 12 |
+
},
|
| 13 |
+
"aliases": {
|
| 14 |
+
"components": "@/components",
|
| 15 |
+
"utils": "@/lib/utils",
|
| 16 |
+
"ui": "@/components/ui",
|
| 17 |
+
"lib": "@/lib",
|
| 18 |
+
"hooks": "@/hooks"
|
| 19 |
+
},
|
| 20 |
+
"iconLibrary": "lucide"
|
| 21 |
+
}
|
eslint.config.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { dirname } from "path";
|
| 2 |
+
import { fileURLToPath } from "url";
|
| 3 |
+
import { FlatCompat } from "@eslint/eslintrc";
|
| 4 |
+
|
| 5 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 6 |
+
const __dirname = dirname(__filename);
|
| 7 |
+
|
| 8 |
+
const compat = new FlatCompat({
|
| 9 |
+
baseDirectory: __dirname,
|
| 10 |
+
});
|
| 11 |
+
|
| 12 |
+
const eslintConfig = [
|
| 13 |
+
...compat.extends("next/core-web-vitals", "next/typescript"),
|
| 14 |
+
];
|
| 15 |
+
|
| 16 |
+
export default eslintConfig;
|
next.config.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { NextConfig } from "next";
|
| 2 |
+
|
| 3 |
+
const nextConfig: NextConfig = {
|
| 4 |
+
/* config options here */
|
| 5 |
+
output: "standalone",
|
| 6 |
+
};
|
| 7 |
+
|
| 8 |
+
export default nextConfig;
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "byte-riot",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "next dev --turbopack",
|
| 7 |
+
"build": "next build",
|
| 8 |
+
"start": "next start",
|
| 9 |
+
"lint": "next lint",
|
| 10 |
+
"lint:fix": "next lint --fix",
|
| 11 |
+
"db:generate": "prisma generate",
|
| 12 |
+
"db:push": "prisma db push",
|
| 13 |
+
"db:pull": "prisma db pull",
|
| 14 |
+
"db:migrate": "prisma migrate dev",
|
| 15 |
+
"db:studio": "prisma studio",
|
| 16 |
+
"db:reset": "prisma migrate reset"
|
| 17 |
+
},
|
| 18 |
+
"dependencies": {
|
| 19 |
+
"@prisma/adapter-pg": "^7.4.1",
|
| 20 |
+
"@prisma/client": "^7.4.1",
|
| 21 |
+
"@radix-ui/react-accordion": "1.2.2",
|
| 22 |
+
"@radix-ui/react-alert-dialog": "1.1.4",
|
| 23 |
+
"@radix-ui/react-aspect-ratio": "1.1.1",
|
| 24 |
+
"@radix-ui/react-avatar": "1.1.2",
|
| 25 |
+
"@radix-ui/react-checkbox": "1.1.3",
|
| 26 |
+
"@radix-ui/react-collapsible": "1.1.2",
|
| 27 |
+
"@radix-ui/react-context-menu": "2.2.4",
|
| 28 |
+
"@radix-ui/react-dialog": "1.1.4",
|
| 29 |
+
"@radix-ui/react-dropdown-menu": "2.1.4",
|
| 30 |
+
"@radix-ui/react-hover-card": "1.1.4",
|
| 31 |
+
"@radix-ui/react-label": "2.1.1",
|
| 32 |
+
"@radix-ui/react-menubar": "1.1.4",
|
| 33 |
+
"@radix-ui/react-navigation-menu": "1.2.3",
|
| 34 |
+
"@radix-ui/react-popover": "1.1.4",
|
| 35 |
+
"@radix-ui/react-progress": "1.1.1",
|
| 36 |
+
"@radix-ui/react-radio-group": "1.2.2",
|
| 37 |
+
"@radix-ui/react-scroll-area": "1.2.2",
|
| 38 |
+
"@radix-ui/react-select": "2.1.4",
|
| 39 |
+
"@radix-ui/react-separator": "1.1.1",
|
| 40 |
+
"@radix-ui/react-slider": "1.2.2",
|
| 41 |
+
"@radix-ui/react-slot": "1.1.1",
|
| 42 |
+
"@radix-ui/react-switch": "1.1.2",
|
| 43 |
+
"@radix-ui/react-tabs": "1.1.2",
|
| 44 |
+
"@radix-ui/react-toast": "1.2.4",
|
| 45 |
+
"@radix-ui/react-toggle": "1.1.1",
|
| 46 |
+
"@radix-ui/react-toggle-group": "1.1.1",
|
| 47 |
+
"@radix-ui/react-tooltip": "1.1.6",
|
| 48 |
+
"@tanstack/react-query": "^5.90.21",
|
| 49 |
+
"class-variance-authority": "^0.7.1",
|
| 50 |
+
"clsx": "^2.1.1",
|
| 51 |
+
"cmdk": "^1.1.1",
|
| 52 |
+
"lucide-react": "^0.525.0",
|
| 53 |
+
"next": "15.4.2",
|
| 54 |
+
"pg": "^8.18.0",
|
| 55 |
+
"react": "19.1.0",
|
| 56 |
+
"react-dom": "19.1.0",
|
| 57 |
+
"react-dropzone": "^14.3.8",
|
| 58 |
+
"react-hook-form": "^7.68.0",
|
| 59 |
+
"recharts": "^3.4.1",
|
| 60 |
+
"sharp": "^0.34.5",
|
| 61 |
+
"tailwind-merge": "^3.3.1"
|
| 62 |
+
},
|
| 63 |
+
"devDependencies": {
|
| 64 |
+
"@eslint/eslintrc": "^3",
|
| 65 |
+
"@tailwindcss/postcss": "^4",
|
| 66 |
+
"@types/node": "^20",
|
| 67 |
+
"@types/pg": "^8.16.0",
|
| 68 |
+
"@types/react": "^19",
|
| 69 |
+
"@types/react-dom": "^19",
|
| 70 |
+
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
| 71 |
+
"@typescript-eslint/parser": "^8.56.0",
|
| 72 |
+
"eslint": "^9.39.3",
|
| 73 |
+
"eslint-config-next": "^15.4.2",
|
| 74 |
+
"prisma": "^7.4.1",
|
| 75 |
+
"tailwindcss": "^4",
|
| 76 |
+
"tw-animate-css": "^1.3.5",
|
| 77 |
+
"typescript": "^5"
|
| 78 |
+
}
|
| 79 |
+
}
|
postcss.config.mjs
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const config = {
|
| 2 |
+
plugins: ["@tailwindcss/postcss"],
|
| 3 |
+
};
|
| 4 |
+
|
| 5 |
+
export default config;
|
prisma.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import path from "node:path";
|
| 2 |
+
import { defineConfig } from "prisma/config";
|
| 3 |
+
import { PrismaPg } from "@prisma/adapter-pg";
|
| 4 |
+
|
| 5 |
+
export default defineConfig({
|
| 6 |
+
earlyAccess: true,
|
| 7 |
+
schema: path.join("prisma", "schema.prisma"),
|
| 8 |
+
migrate: {
|
| 9 |
+
async adapter(env) {
|
| 10 |
+
return new PrismaPg({ connectionString: env.DATABASE_URL });
|
| 11 |
+
},
|
| 12 |
+
},
|
| 13 |
+
});
|
prisma/schema.prisma
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
generator client {
|
| 2 |
+
provider = "prisma-client-js"
|
| 3 |
+
output = "../src/generated/prisma"
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
datasource db {
|
| 7 |
+
provider = "postgresql"
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
model cv_file {
|
| 11 |
+
file_id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
| 12 |
+
user_id String @db.Uuid
|
| 13 |
+
file_type String @db.VarChar
|
| 14 |
+
filename String @db.VarChar
|
| 15 |
+
url String @db.VarChar
|
| 16 |
+
is_extracted Boolean
|
| 17 |
+
uploaded_at DateTime? @default(dbgenerated("timezone('Asia/Jakarta'::text, now())")) @db.Timestamptz(6)
|
| 18 |
+
date_modified DateTime? @default(dbgenerated("timezone('Asia/Jakarta'::text, now())")) @db.Timestamptz(6)
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
model cv_filter {
|
| 22 |
+
criteria_id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
| 23 |
+
gpa_edu_1 Float?
|
| 24 |
+
gpa_edu_2 Float?
|
| 25 |
+
gpa_edu_3 Float?
|
| 26 |
+
univ_edu_1 String? @db.VarChar
|
| 27 |
+
univ_edu_2 String? @db.VarChar
|
| 28 |
+
univ_edu_3 String? @db.VarChar
|
| 29 |
+
major_edu_1 String? @db.VarChar
|
| 30 |
+
major_edu_2 String? @db.VarChar
|
| 31 |
+
major_edu_3 String? @db.VarChar
|
| 32 |
+
domicile String? @db.VarChar
|
| 33 |
+
yoe Int?
|
| 34 |
+
hardskills String[] @db.VarChar
|
| 35 |
+
softskills String[] @db.VarChar
|
| 36 |
+
certifications String[] @db.VarChar
|
| 37 |
+
business_domain String[] @db.VarChar
|
| 38 |
+
created_at DateTime? @default(dbgenerated("timezone('Asia/Jakarta'::text, now())")) @db.Timestamptz(6)
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
model cv_matching {
|
| 42 |
+
matching_id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
| 43 |
+
profile_id String? @db.Uuid
|
| 44 |
+
criteria_id String? @db.Uuid
|
| 45 |
+
gpa_edu_1 Boolean?
|
| 46 |
+
gpa_edu_2 Boolean?
|
| 47 |
+
gpa_edu_3 Boolean?
|
| 48 |
+
univ_edu_1 Boolean?
|
| 49 |
+
univ_edu_2 Boolean?
|
| 50 |
+
univ_edu_3 Boolean?
|
| 51 |
+
major_edu_1 Boolean?
|
| 52 |
+
major_edu_2 Boolean?
|
| 53 |
+
major_edu_3 Boolean?
|
| 54 |
+
domicile Boolean?
|
| 55 |
+
yoe Boolean?
|
| 56 |
+
hardskills Boolean?
|
| 57 |
+
softskills Boolean?
|
| 58 |
+
certifications Boolean?
|
| 59 |
+
business_domain Boolean?
|
| 60 |
+
created_at DateTime? @default(dbgenerated("timezone('Asia/Jakarta'::text, now())")) @db.Timestamptz(6)
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
model cv_profile {
|
| 64 |
+
profile_id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
| 65 |
+
fullname String @db.VarChar
|
| 66 |
+
gpa_edu_1 Float?
|
| 67 |
+
univ_edu_1 String? @db.VarChar
|
| 68 |
+
major_edu_1 String? @db.VarChar
|
| 69 |
+
gpa_edu_2 Float?
|
| 70 |
+
univ_edu_2 String? @db.VarChar
|
| 71 |
+
major_edu_2 String? @db.VarChar
|
| 72 |
+
gpa_edu_3 Float?
|
| 73 |
+
univ_edu_3 String? @db.VarChar
|
| 74 |
+
major_edu_3 String? @db.VarChar
|
| 75 |
+
domicile String? @db.VarChar
|
| 76 |
+
yoe Int?
|
| 77 |
+
hardskills String[] @db.VarChar
|
| 78 |
+
softskills String[] @db.VarChar
|
| 79 |
+
certifications String[] @db.VarChar
|
| 80 |
+
business_domain String[] @db.VarChar
|
| 81 |
+
filename String @unique @db.VarChar
|
| 82 |
+
file_id String? @db.Uuid
|
| 83 |
+
created_at DateTime? @default(dbgenerated("timezone('Asia/Jakarta'::text, now())")) @db.Timestamptz(6)
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
model cv_score {
|
| 87 |
+
scoring_id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
| 88 |
+
matching_id String? @db.Uuid
|
| 89 |
+
score Int?
|
| 90 |
+
created_at DateTime? @default(dbgenerated("timezone('Asia/Jakarta'::text, now())")) @db.Timestamptz(6)
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
model cv_tenant {
|
| 94 |
+
tenant_id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
| 95 |
+
tenant_name String @unique @db.VarChar
|
| 96 |
+
created_at DateTime? @default(dbgenerated("timezone('Asia/Jakarta'::text, now())")) @db.Timestamptz(6)
|
| 97 |
+
date_modified DateTime? @default(dbgenerated("timezone('Asia/Jakarta'::text, now())")) @db.Timestamptz(6)
|
| 98 |
+
notes String? @db.VarChar
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
model cv_user {
|
| 102 |
+
user_id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
| 103 |
+
username String @unique @db.VarChar
|
| 104 |
+
hashed_password String @db.VarChar
|
| 105 |
+
email String @unique @db.VarChar
|
| 106 |
+
full_name String @db.VarChar
|
| 107 |
+
role String @db.VarChar
|
| 108 |
+
is_active Boolean
|
| 109 |
+
tenant_id String? @db.Uuid
|
| 110 |
+
created_at DateTime? @default(dbgenerated("timezone('Asia/Jakarta'::text, now())")) @db.Timestamptz(6)
|
| 111 |
+
date_modified DateTime? @default(dbgenerated("timezone('Asia/Jakarta'::text, now())")) @db.Timestamptz(6)
|
| 112 |
+
notes String? @db.VarChar
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
model cv_weight {
|
| 116 |
+
weight_id String @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
| 117 |
+
criteria_id String @db.Uuid
|
| 118 |
+
gpa_edu_1 Float?
|
| 119 |
+
gpa_edu_2 Float?
|
| 120 |
+
gpa_edu_3 Float?
|
| 121 |
+
univ_edu_1 Float?
|
| 122 |
+
univ_edu_2 Float?
|
| 123 |
+
univ_edu_3 Float?
|
| 124 |
+
major_edu_1 Float?
|
| 125 |
+
major_edu_2 Float?
|
| 126 |
+
major_edu_3 Float?
|
| 127 |
+
domicile Float?
|
| 128 |
+
yoe Float?
|
| 129 |
+
hardskills Float?
|
| 130 |
+
softskills Float?
|
| 131 |
+
certifications Float?
|
| 132 |
+
business_domain Float?
|
| 133 |
+
created_at DateTime? @default(dbgenerated("timezone('Asia/Jakarta'::text, now())")) @db.Timestamptz(6)
|
| 134 |
+
|
| 135 |
+
@@id([weight_id, criteria_id])
|
| 136 |
+
}
|
public/avatar.png
ADDED
|
|
public/book.svg
ADDED
|
|
public/chat.svg
ADDED
|
|
public/dashboard/Background.png
ADDED
|
public/dashboard/Background.webp
ADDED
|
public/dashboard/logo.png
ADDED
|
public/description.svg
ADDED
|
|
public/doc.svg
ADDED
|
|
public/duration.svg
ADDED
|
|
public/file.svg
ADDED
|
|
public/globe.svg
ADDED
|
|
public/loading1.png
ADDED
|
public/loading2.png
ADDED
|
public/logo.png
ADDED
|
public/logo.svg
ADDED
|
|
public/next.svg
ADDED
|
|
public/notifications.svg
ADDED
|
|
public/paragraph.svg
ADDED
|
|
public/robot-1.png
ADDED
|
public/vercel.svg
ADDED
|
|
public/video.svg
ADDED
|
|
public/window.svg
ADDED
|
|
src/app/api/agentic/create_weight/route.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server"
|
| 2 |
+
|
| 3 |
+
export async function POST(request: NextRequest) {
|
| 4 |
+
const token = request.headers.get("Authorization")
|
| 5 |
+
const { searchParams } = new URL(request.url)
|
| 6 |
+
const criteria_id = searchParams.get("criteria_id")
|
| 7 |
+
|
| 8 |
+
const body = await request.json()
|
| 9 |
+
|
| 10 |
+
const res = await fetch(
|
| 11 |
+
`https://byteriot-candidateexplorer.hf.space/CandidateExplorer/agentic/create_weight?criteria_id=${criteria_id}`,
|
| 12 |
+
{
|
| 13 |
+
method: "POST",
|
| 14 |
+
headers: {
|
| 15 |
+
"Content-Type": "application/json",
|
| 16 |
+
Authorization: token ?? "",
|
| 17 |
+
},
|
| 18 |
+
body: JSON.stringify(body),
|
| 19 |
+
}
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
const data = await res.json()
|
| 23 |
+
return NextResponse.json(data, { status: res.status })
|
| 24 |
+
}
|
src/app/api/cv-profile/options/route.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from "next/server";
|
| 2 |
+
import { prisma } from "@/lib/prisma";
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
export async function GET() {
|
| 6 |
+
console.log("DATABASE_URL:", process.env.DATABASE_URL);
|
| 7 |
+
try {
|
| 8 |
+
const profiles = await prisma.cv_profile.findMany({
|
| 9 |
+
select: {
|
| 10 |
+
domicile: true,
|
| 11 |
+
softskills: true,
|
| 12 |
+
certifications: true,
|
| 13 |
+
business_domain: true,
|
| 14 |
+
univ_edu_1: true,
|
| 15 |
+
univ_edu_2: true,
|
| 16 |
+
univ_edu_3: true,
|
| 17 |
+
major_edu_1: true,
|
| 18 |
+
major_edu_2: true,
|
| 19 |
+
major_edu_3: true,
|
| 20 |
+
},
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
// Flatten and deduplicate array fields
|
| 24 |
+
const unique = (arr: (string | null)[]) =>
|
| 25 |
+
[...new Set(arr.filter(Boolean))].sort() as string[];
|
| 26 |
+
|
| 27 |
+
const flatArray = (key: keyof typeof profiles[0]) =>
|
| 28 |
+
unique(profiles.flatMap((p) => p[key] as string[]));
|
| 29 |
+
|
| 30 |
+
const options = {
|
| 31 |
+
domicile: unique(profiles.map((p) => p.domicile)),
|
| 32 |
+
softskills: flatArray("softskills"),
|
| 33 |
+
certifications: flatArray("certifications"),
|
| 34 |
+
business_domain: flatArray("business_domain"),
|
| 35 |
+
univ_edu: unique([
|
| 36 |
+
...profiles.map((p) => p.univ_edu_1),
|
| 37 |
+
...profiles.map((p) => p.univ_edu_2),
|
| 38 |
+
...profiles.map((p) => p.univ_edu_3),
|
| 39 |
+
]),
|
| 40 |
+
major_edu: unique([
|
| 41 |
+
...profiles.map((p) => p.major_edu_1),
|
| 42 |
+
...profiles.map((p) => p.major_edu_2),
|
| 43 |
+
...profiles.map((p) => p.major_edu_3),
|
| 44 |
+
]),
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
return NextResponse.json(options);
|
| 48 |
+
} catch (error) {
|
| 49 |
+
console.error(error);
|
| 50 |
+
return NextResponse.json({ error: "Failed to fetch options" }, { status: 500 });
|
| 51 |
+
}
|
| 52 |
+
}
|
src/app/api/cv-profile/route.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { prisma } from "@/lib/prisma";
|
| 2 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 3 |
+
|
| 4 |
+
async function getMatchingArrayValues(
|
| 5 |
+
column: string,
|
| 6 |
+
search: string,
|
| 7 |
+
prisma: any
|
| 8 |
+
): Promise<string[]> {
|
| 9 |
+
const result = await prisma.$queryRawUnsafe<{ val: string }[]>(`
|
| 10 |
+
SELECT DISTINCT unnest(${column}) as val
|
| 11 |
+
FROM cv_profile
|
| 12 |
+
WHERE EXISTS (
|
| 13 |
+
SELECT 1 FROM unnest(${column}) AS elem
|
| 14 |
+
WHERE elem ILIKE '%' || $1 || '%'
|
| 15 |
+
)
|
| 16 |
+
`, search);
|
| 17 |
+
|
| 18 |
+
// Filter in JS for partial match
|
| 19 |
+
return result
|
| 20 |
+
.map((r) => r.val)
|
| 21 |
+
.filter((v) => v.toLowerCase().includes(search.toLowerCase()));
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export async function GET(request: NextRequest) {
|
| 25 |
+
const { searchParams } = new URL(request.url);
|
| 26 |
+
|
| 27 |
+
// --- PAGINATION ---
|
| 28 |
+
const page = Number.parseInt(searchParams.get("page") ?? "1");
|
| 29 |
+
const limit = Number.parseInt(searchParams.get("limit") ?? "10");
|
| 30 |
+
const skip = (page - 1) * limit;
|
| 31 |
+
|
| 32 |
+
// --- SEARCH ---
|
| 33 |
+
const search = searchParams.get("search");
|
| 34 |
+
|
| 35 |
+
// --- FILTERS ---
|
| 36 |
+
const domicile = searchParams.get("domicile");
|
| 37 |
+
const yoe_min = searchParams.get("yoe_min");
|
| 38 |
+
const yoe_max = searchParams.get("yoe_max");
|
| 39 |
+
const softskills = searchParams.getAll("softskills");
|
| 40 |
+
const certifications = searchParams.getAll("certifications");
|
| 41 |
+
const business_domain = searchParams.getAll("business_domain");
|
| 42 |
+
|
| 43 |
+
const univ_edu_1 = searchParams.get("univ_edu_1");
|
| 44 |
+
const univ_edu_2 = searchParams.get("univ_edu_2");
|
| 45 |
+
const univ_edu_3 = searchParams.get("univ_edu_3");
|
| 46 |
+
const major_edu_1 = searchParams.get("major_edu_1");
|
| 47 |
+
const major_edu_2 = searchParams.get("major_edu_2");
|
| 48 |
+
const major_edu_3 = searchParams.get("major_edu_3");
|
| 49 |
+
const gpa_min_1 = searchParams.get("gpa_min_1");
|
| 50 |
+
const gpa_max_1 = searchParams.get("gpa_max_1");
|
| 51 |
+
const gpa_min_2 = searchParams.get("gpa_min_2");
|
| 52 |
+
const gpa_max_2 = searchParams.get("gpa_max_2");
|
| 53 |
+
const gpa_min_3 = searchParams.get("gpa_min_3");
|
| 54 |
+
const gpa_max_3 = searchParams.get("gpa_max_3");
|
| 55 |
+
|
| 56 |
+
// --- SORT ---
|
| 57 |
+
const sortBy = searchParams.get("sortBy") ?? "created_at";
|
| 58 |
+
const sortOrder = searchParams.get("sortOrder") === "asc" ? "asc" : "desc";
|
| 59 |
+
|
| 60 |
+
const allowedSortFields = [
|
| 61 |
+
"fullname", "domicile", "yoe", "gpa_edu_1", "gpa_edu_2", "gpa_edu_3",
|
| 62 |
+
"univ_edu_1", "univ_edu_2", "univ_edu_3", "major_edu_1", "major_edu_2",
|
| 63 |
+
"major_edu_3", "created_at",
|
| 64 |
+
];
|
| 65 |
+
|
| 66 |
+
const orderBy = allowedSortFields.includes(sortBy)
|
| 67 |
+
? { [sortBy]: sortOrder }
|
| 68 |
+
: { created_at: "desc" as const };
|
| 69 |
+
|
| 70 |
+
// --- BUILD WHERE ---
|
| 71 |
+
const where: any = {
|
| 72 |
+
// Search across all text columns
|
| 73 |
+
...(search ? {
|
| 74 |
+
OR: [
|
| 75 |
+
{ fullname: { contains: search, mode: "insensitive" } },
|
| 76 |
+
{ domicile: { contains: search, mode: "insensitive" } },
|
| 77 |
+
{ univ_edu_1: { contains: search, mode: "insensitive" } },
|
| 78 |
+
{ univ_edu_2: { contains: search, mode: "insensitive" } },
|
| 79 |
+
{ univ_edu_3: { contains: search, mode: "insensitive" } },
|
| 80 |
+
{ major_edu_1: { contains: search, mode: "insensitive" } },
|
| 81 |
+
{ major_edu_2: { contains: search, mode: "insensitive" } },
|
| 82 |
+
{ major_edu_3: { contains: search, mode: "insensitive" } },
|
| 83 |
+
{ filename: { contains: search, mode: "insensitive" } },
|
| 84 |
+
|
| 85 |
+
// ✅ Array partial + case-insensitive search using raw filter
|
| 86 |
+
{
|
| 87 |
+
hardskills: {
|
| 88 |
+
hasSome: await getMatchingArrayValues("hardskills", search, prisma),
|
| 89 |
+
},
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
softskills: {
|
| 93 |
+
hasSome: await getMatchingArrayValues("softskills", search, prisma),
|
| 94 |
+
},
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
certifications: {
|
| 98 |
+
hasSome: await getMatchingArrayValues("certifications", search, prisma),
|
| 99 |
+
},
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
business_domain: {
|
| 103 |
+
hasSome: await getMatchingArrayValues("business_domain", search, prisma),
|
| 104 |
+
},
|
| 105 |
+
},
|
| 106 |
+
|
| 107 |
+
// GPA — only search if input is a valid number
|
| 108 |
+
...(Number.isNaN(Number.Number.parseFloat(search)) ? [] : [
|
| 109 |
+
{ gpa_edu_1: { equals: Number.Number.parseFloat(search) } },
|
| 110 |
+
{ gpa_edu_2: { equals: Number.Number.parseFloat(search) } },
|
| 111 |
+
{ gpa_edu_3: { equals: Number.Number.parseFloat(search) } },
|
| 112 |
+
]),
|
| 113 |
+
|
| 114 |
+
// YOE — only search if input is a valid integer
|
| 115 |
+
...(Number.isNaN(Number.parseInt(search)) ? [] : [
|
| 116 |
+
{ yoe: { equals: Number.parseInt(search) } },
|
| 117 |
+
]),
|
| 118 |
+
],
|
| 119 |
+
} : {}),
|
| 120 |
+
|
| 121 |
+
...(domicile && { domicile }),
|
| 122 |
+
|
| 123 |
+
...(yoe_min || yoe_max
|
| 124 |
+
? {
|
| 125 |
+
yoe: {
|
| 126 |
+
...(yoe_min && { gte: Number.parseInt(yoe_min) }),
|
| 127 |
+
...(yoe_max && { lte: Number.parseInt(yoe_max) }),
|
| 128 |
+
},
|
| 129 |
+
}
|
| 130 |
+
: {}),
|
| 131 |
+
|
| 132 |
+
...(softskills.length > 0 && {
|
| 133 |
+
softskills: { hasSome: softskills },
|
| 134 |
+
}),
|
| 135 |
+
|
| 136 |
+
...(certifications.length > 0 && {
|
| 137 |
+
certifications: { hasSome: certifications },
|
| 138 |
+
}),
|
| 139 |
+
|
| 140 |
+
...(business_domain.length > 0 && {
|
| 141 |
+
business_domain: { hasSome: business_domain },
|
| 142 |
+
}),
|
| 143 |
+
|
| 144 |
+
...(univ_edu_1 && { univ_edu_1 }),
|
| 145 |
+
...(major_edu_1 && { major_edu_1 }),
|
| 146 |
+
...((gpa_min_1 || gpa_max_1) && {
|
| 147 |
+
gpa_edu_1: {
|
| 148 |
+
...(gpa_min_1 && { gte: Number.parseFloat(gpa_min_1) }),
|
| 149 |
+
...(gpa_max_1 && { lte: Number.parseFloat(gpa_max_1) }),
|
| 150 |
+
},
|
| 151 |
+
}),
|
| 152 |
+
|
| 153 |
+
...(univ_edu_2 && { univ_edu_2 }),
|
| 154 |
+
...(major_edu_2 && { major_edu_2 }),
|
| 155 |
+
...((gpa_min_2 || gpa_max_2) && {
|
| 156 |
+
gpa_edu_2: {
|
| 157 |
+
...(gpa_min_2 && { gte: Number.parseFloat(gpa_min_2) }),
|
| 158 |
+
...(gpa_max_2 && { lte: Number.parseFloat(gpa_max_2) }),
|
| 159 |
+
},
|
| 160 |
+
}),
|
| 161 |
+
|
| 162 |
+
...(univ_edu_3 && { univ_edu_3 }),
|
| 163 |
+
...(major_edu_3 && { major_edu_3 }),
|
| 164 |
+
...((gpa_min_3 || gpa_max_3) && {
|
| 165 |
+
gpa_edu_3: {
|
| 166 |
+
...(gpa_min_3 && { gte: Number.parseFloat(gpa_min_3) }),
|
| 167 |
+
...(gpa_max_3 && { lte: Number.parseFloat(gpa_max_3) }),
|
| 168 |
+
},
|
| 169 |
+
}),
|
| 170 |
+
};
|
| 171 |
+
|
| 172 |
+
try {
|
| 173 |
+
const [profiles, total] = await Promise.all([
|
| 174 |
+
prisma.cv_profile.findMany({
|
| 175 |
+
where,
|
| 176 |
+
orderBy,
|
| 177 |
+
skip,
|
| 178 |
+
take: limit,
|
| 179 |
+
}),
|
| 180 |
+
prisma.cv_profile.count({ where }),
|
| 181 |
+
]);
|
| 182 |
+
|
| 183 |
+
return NextResponse.json({
|
| 184 |
+
data: profiles,
|
| 185 |
+
pagination: {
|
| 186 |
+
total,
|
| 187 |
+
page,
|
| 188 |
+
limit,
|
| 189 |
+
totalPages: Math.ceil(total / limit),
|
| 190 |
+
hasNext: page < Math.ceil(total / limit),
|
| 191 |
+
hasPrev: page > 1,
|
| 192 |
+
},
|
| 193 |
+
});
|
| 194 |
+
} catch (error) {
|
| 195 |
+
console.error(error);
|
| 196 |
+
return NextResponse.json({ error: "Failed to fetch profiles" }, { status: 500 });
|
| 197 |
+
}
|
| 198 |
+
}
|
src/app/api/me/route.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server"
|
| 2 |
+
|
| 3 |
+
export async function GET(request: NextRequest) {
|
| 4 |
+
const token = request.headers.get("Authorization")
|
| 5 |
+
|
| 6 |
+
if (!token) {
|
| 7 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const res = await fetch("https://byteriot-candidateexplorer.hf.space/CandidateExplorer/admin/me", {
|
| 11 |
+
headers: {
|
| 12 |
+
Authorization: token,
|
| 13 |
+
},
|
| 14 |
+
})
|
| 15 |
+
|
| 16 |
+
if (!res.ok) {
|
| 17 |
+
return NextResponse.json({ error: "Failed to fetch user" }, { status: res.status })
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
const data = await res.json()
|
| 21 |
+
return NextResponse.json(data)
|
| 22 |
+
}
|
src/app/favicon.ico
ADDED
|
|
src/app/globals.css
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
| 2 |
+
@import "tw-animate-css";
|
| 3 |
+
|
| 4 |
+
@custom-variant dark (&:is(.dark *));
|
| 5 |
+
|
| 6 |
+
@theme inline {
|
| 7 |
+
--color-background: var(--background);
|
| 8 |
+
--color-foreground: var(--foreground);
|
| 9 |
+
--font-sans: var(--font-geist-sans);
|
| 10 |
+
--font-poppins: var(--font-poppins);
|
| 11 |
+
--font-mono: var(--font-geist-mono);
|
| 12 |
+
--color-primary: #18AF4A;
|
| 13 |
+
--color-primary-dark: #000f24;
|
| 14 |
+
--color-primary-darker: #010725;
|
| 15 |
+
/* --color-accent: #297c9d; */
|
| 16 |
+
--color-accent-dark: #2b2b8e;
|
| 17 |
+
--color-accent-darker: #3C260A;
|
| 18 |
+
|
| 19 |
+
--color-transparent: transparent;
|
| 20 |
+
|
| 21 |
+
--color-neutral-50: #2a292e;
|
| 22 |
+
--color-neutral-100: #3a3941;
|
| 23 |
+
--color-neutral-200: #43414b;
|
| 24 |
+
--color-neutral-300: #4e4c58;
|
| 25 |
+
--color-neutral-400: #5f5d6c;
|
| 26 |
+
--color-neutral-500: #767483;
|
| 27 |
+
--color-neutral-600: #92919f;
|
| 28 |
+
--color-neutral-700: #b9b8c1;
|
| 29 |
+
--color-neutral-800: #dad9de;
|
| 30 |
+
--color-neutral-900: #efeef0;
|
| 31 |
+
--color-neutral-950: #f7f7f8;
|
| 32 |
+
|
| 33 |
+
--color-effect-color-glow-neutral: rgba(145, 158, 171, 0.15);
|
| 34 |
+
|
| 35 |
+
--animate-shine: shine 2s infinite;
|
| 36 |
+
--animate-wiggle: wiggle 1s ease-in-out infinite;
|
| 37 |
+
|
| 38 |
+
@keyframes shine {
|
| 39 |
+
0% { background-position: -200% 0; }
|
| 40 |
+
100% { background-position: 200% 0; }
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
@keyframes wiggle {
|
| 44 |
+
0%,
|
| 45 |
+
100% { transform: rotate(-3deg); }
|
| 46 |
+
50% { transform: rotate(3deg); }
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
--gradient-radial-to-b: circle farthest-side at top center;
|
| 50 |
+
--gradient-radial-to-t: circle farthest-side at bottom center;
|
| 51 |
+
--gradient-radial-to-r: circle farthest-side at center right;
|
| 52 |
+
--gradient-radial-to-l: circle farthest-side at center left;
|
| 53 |
+
--shine-effect: linear-gradient(30deg, transparent 25%, rgba(255,255,255,0.05) 45%, rgba(255,255,255,0.1) 50%, rgba(124,150,253,0.2) 58%, rgba(255,255,255,0.05) 60%, transparent 75%);
|
| 54 |
+
|
| 55 |
+
--color-sidebar-ring: var(--sidebar-ring);
|
| 56 |
+
--color-sidebar-border: var(--sidebar-border);
|
| 57 |
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
| 58 |
+
--color-sidebar-accent: var(--sidebar-accent);
|
| 59 |
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
| 60 |
+
--color-sidebar-primary: var(--sidebar-primary);
|
| 61 |
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
| 62 |
+
--color-sidebar: var(--sidebar);
|
| 63 |
+
--color-chart-5: var(--chart-5);
|
| 64 |
+
--color-chart-4: var(--chart-4);
|
| 65 |
+
--color-chart-3: var(--chart-3);
|
| 66 |
+
--color-chart-2: var(--chart-2);
|
| 67 |
+
--color-chart-1: var(--chart-1);
|
| 68 |
+
--color-ring: var(--ring);
|
| 69 |
+
--color-input: var(--input);
|
| 70 |
+
--color-border: var(--border);
|
| 71 |
+
--color-destructive: var(--destructive);
|
| 72 |
+
--color-accent-foreground: var(--accent-foreground);
|
| 73 |
+
--color-muted-foreground: var(--muted-foreground);
|
| 74 |
+
--color-muted: var(--muted);
|
| 75 |
+
--color-secondary-foreground: var(--secondary-foreground);
|
| 76 |
+
--color-secondary: var(--secondary);
|
| 77 |
+
--color-primary-foreground: var(--primary-foreground);
|
| 78 |
+
--color-popover-foreground: var(--popover-foreground);
|
| 79 |
+
--color-popover: var(--popover);
|
| 80 |
+
--color-card-foreground: var(--card-foreground);
|
| 81 |
+
--color-card: var(--card);
|
| 82 |
+
--radius-sm: calc(var(--radius) - 4px);
|
| 83 |
+
--radius-md: calc(var(--radius) - 2px);
|
| 84 |
+
--radius-lg: var(--radius);
|
| 85 |
+
--radius-xl: calc(var(--radius) + 4px);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.nav-shadow {
|
| 89 |
+
box-shadow: 0px 4px 6px -1px var(--color-effect-color-glow-neutral, rgba(145, 158, 171, 0.15)), 0px 2px 4px -1px var(--color-effect-color-glow-neutral, rgba(145, 158, 171, 0.15));
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
body {
|
| 93 |
+
height: 100%;
|
| 94 |
+
margin: 0;
|
| 95 |
+
width: 100%;
|
| 96 |
+
display: flex;
|
| 97 |
+
flex-direction: column;
|
| 98 |
+
font-family: var(--font-poppins);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
nav {
|
| 102 |
+
padding: 12px;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
h1 {
|
| 106 |
+
font-weight: 700;
|
| 107 |
+
/* font-family: var(--font-poppins); */
|
| 108 |
+
line-height: 32px;
|
| 109 |
+
}
|
| 110 |
+
/* p {
|
| 111 |
+
font-family: var(--font-poppins);
|
| 112 |
+
} */
|
| 113 |
+
|
| 114 |
+
:root {
|
| 115 |
+
--radius: 0.625rem;
|
| 116 |
+
--background: oklch(1 0 0);
|
| 117 |
+
--foreground: #2C3947;
|
| 118 |
+
--card: oklch(1 0 0);
|
| 119 |
+
--card-foreground: oklch(0.145 0 0);
|
| 120 |
+
--popover: oklch(1 0 0);
|
| 121 |
+
--popover-foreground: oklch(0.145 0 0);
|
| 122 |
+
--primary: oklch(0.205 0 0);
|
| 123 |
+
--primary-foreground: oklch(0.985 0 0);
|
| 124 |
+
--secondary: oklch(0.97 0 0);
|
| 125 |
+
--secondary-foreground: oklch(0.205 0 0);
|
| 126 |
+
--muted: oklch(0.97 0 0);
|
| 127 |
+
--muted-foreground: oklch(0.556 0 0);
|
| 128 |
+
--accent: oklch(0.97 0 0);
|
| 129 |
+
--accent-foreground: oklch(0.205 0 0);
|
| 130 |
+
--destructive: oklch(0.577 0.245 27.325);
|
| 131 |
+
--border: oklch(0.922 0 0);
|
| 132 |
+
--input: oklch(0.922 0 0);
|
| 133 |
+
--ring: oklch(0.708 0 0);
|
| 134 |
+
--chart-1: oklch(0.646 0.222 41.116);
|
| 135 |
+
--chart-2: oklch(0.6 0.118 184.704);
|
| 136 |
+
--chart-3: oklch(0.398 0.07 227.392);
|
| 137 |
+
--chart-4: oklch(0.828 0.189 84.429);
|
| 138 |
+
--chart-5: oklch(0.769 0.188 70.08);
|
| 139 |
+
--sidebar: oklch(0.985 0 0);
|
| 140 |
+
--sidebar-foreground: oklch(0.145 0 0);
|
| 141 |
+
--sidebar-primary: oklch(0.205 0 0);
|
| 142 |
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
| 143 |
+
--sidebar-accent: oklch(0.97 0 0);
|
| 144 |
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
| 145 |
+
--sidebar-border: oklch(0.922 0 0);
|
| 146 |
+
--sidebar-ring: oklch(0.708 0 0);}
|
| 147 |
+
|
| 148 |
+
.dark {
|
| 149 |
+
--background: oklch(0.145 0 0);
|
| 150 |
+
--foreground: oklch(0.985 0 0);
|
| 151 |
+
--card: oklch(0.205 0 0);
|
| 152 |
+
--card-foreground: oklch(0.985 0 0);
|
| 153 |
+
--popover: oklch(0.205 0 0);
|
| 154 |
+
--popover-foreground: oklch(0.985 0 0);
|
| 155 |
+
--primary: oklch(0.922 0 0);
|
| 156 |
+
--primary-foreground: oklch(0.205 0 0);
|
| 157 |
+
--secondary: oklch(0.269 0 0);
|
| 158 |
+
--secondary-foreground: oklch(0.985 0 0);
|
| 159 |
+
--muted: oklch(0.269 0 0);
|
| 160 |
+
--muted-foreground: oklch(0.708 0 0);
|
| 161 |
+
--accent: oklch(0.269 0 0);
|
| 162 |
+
--accent-foreground: oklch(0.985 0 0);
|
| 163 |
+
--destructive: oklch(0.704 0.191 22.216);
|
| 164 |
+
--border: oklch(1 0 0 / 10%);
|
| 165 |
+
--input: oklch(1 0 0 / 15%);
|
| 166 |
+
--ring: oklch(0.556 0 0);
|
| 167 |
+
--chart-1: oklch(0.488 0.243 264.376);
|
| 168 |
+
--chart-2: oklch(0.696 0.17 162.48);
|
| 169 |
+
--chart-3: oklch(0.769 0.188 70.08);
|
| 170 |
+
--chart-4: oklch(0.627 0.265 303.9);
|
| 171 |
+
--chart-5: oklch(0.645 0.246 16.439);
|
| 172 |
+
--sidebar: oklch(0.205 0 0);
|
| 173 |
+
--sidebar-foreground: oklch(0.985 0 0);
|
| 174 |
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
| 175 |
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
| 176 |
+
--sidebar-accent: oklch(0.269 0 0);
|
| 177 |
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
| 178 |
+
--sidebar-border: oklch(1 0 0 / 10%);
|
| 179 |
+
--sidebar-ring: oklch(0.556 0 0);}
|
| 180 |
+
|
| 181 |
+
@layer base {
|
| 182 |
+
* {
|
| 183 |
+
@apply border-border outline-ring/50;}
|
| 184 |
+
body {
|
| 185 |
+
@apply bg-background text-foreground;}}
|
src/app/layout.tsx
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import { Geist, Geist_Mono, Poppins } from "next/font/google";
|
| 3 |
+
import "./globals.css";
|
| 4 |
+
|
| 5 |
+
const geistSans = Geist({
|
| 6 |
+
variable: "--font-geist-sans",
|
| 7 |
+
subsets: ["latin"],
|
| 8 |
+
});
|
| 9 |
+
|
| 10 |
+
const geistMono = Geist_Mono({
|
| 11 |
+
variable: "--font-geist-mono",
|
| 12 |
+
subsets: ["latin"],
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
const poppins = Poppins({
|
| 16 |
+
variable: "--font-poppins",
|
| 17 |
+
subsets: ["latin"],
|
| 18 |
+
// choose only the weights you need — lighter is better for perf
|
| 19 |
+
weight: ["300", "400", "500", "600", "700"],
|
| 20 |
+
display: "swap",
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
export const metadata: Metadata = {
|
| 24 |
+
title: "ByteRiot",
|
| 25 |
+
description: "ByteRiot yooo",
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
export default function RootLayout({
|
| 29 |
+
children,
|
| 30 |
+
}: Readonly<{
|
| 31 |
+
children: React.ReactNode;
|
| 32 |
+
}>) {
|
| 33 |
+
return (
|
| 34 |
+
<html lang="en">
|
| 35 |
+
<body
|
| 36 |
+
className={`${geistSans.variable} ${geistMono.variable} ${poppins.className} antialiased`}
|
| 37 |
+
>
|
| 38 |
+
{children}
|
| 39 |
+
</body>
|
| 40 |
+
</html>
|
| 41 |
+
);
|
| 42 |
+
}
|
src/app/learning/layout.tsx
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import {
|
| 4 |
+
Accordion,
|
| 5 |
+
AccordionContent,
|
| 6 |
+
AccordionItem,
|
| 7 |
+
AccordionTrigger,
|
| 8 |
+
} from "@/components/ui/accordion";
|
| 9 |
+
import { useEffect, useState } from "react";
|
| 10 |
+
import { RoadmapsRoot } from "../onboarding/learningPath";
|
| 11 |
+
|
| 12 |
+
function CourseContentAccordion({
|
| 13 |
+
competencies,
|
| 14 |
+
}: {
|
| 15 |
+
competencies: { skill: string; description: string; resources: string[] }[];
|
| 16 |
+
}) {
|
| 17 |
+
return (
|
| 18 |
+
<div className="flex flex-col w-full gap-6">
|
| 19 |
+
{competencies.map((c, idx) => (
|
| 20 |
+
<Accordion
|
| 21 |
+
type="single"
|
| 22 |
+
collapsible
|
| 23 |
+
className="w-full rounded-[8px]"
|
| 24 |
+
key={c.skill}
|
| 25 |
+
>
|
| 26 |
+
<AccordionItem value={c.skill}>
|
| 27 |
+
<AccordionTrigger className="focus:cursor-pointer p-0">
|
| 28 |
+
<div className="flex flex-row items-start gap-4">
|
| 29 |
+
<div className="flex flex-col gap-2.5">
|
| 30 |
+
<h2 className="text-[16px] font-bold leading-6">{c.skill}</h2>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
</AccordionTrigger>
|
| 34 |
+
<AccordionContent className="flex flex-col gap-4 text-balance mt-4">
|
| 35 |
+
<div className="flex flex-col w-full gap-4">
|
| 36 |
+
{c.resources.map((c, idx) => (
|
| 37 |
+
<p
|
| 38 |
+
className="text-[#637381] text-[14px] leading-5"
|
| 39 |
+
key={`${c}-${idx}`}
|
| 40 |
+
>
|
| 41 |
+
{c}
|
| 42 |
+
</p>
|
| 43 |
+
))}
|
| 44 |
+
</div>
|
| 45 |
+
</AccordionContent>
|
| 46 |
+
</AccordionItem>
|
| 47 |
+
</Accordion>
|
| 48 |
+
))}
|
| 49 |
+
</div>
|
| 50 |
+
);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
export default function Layout({
|
| 54 |
+
children,
|
| 55 |
+
}: Readonly<{
|
| 56 |
+
children: React.ReactNode;
|
| 57 |
+
}>) {
|
| 58 |
+
const [roadmaps, setRoadmaps] = useState<RoadmapsRoot | null>();
|
| 59 |
+
useEffect(() => {
|
| 60 |
+
const data = localStorage.getItem("roadmap");
|
| 61 |
+
if (data) setRoadmaps(JSON.parse(data));
|
| 62 |
+
}, []);
|
| 63 |
+
return (
|
| 64 |
+
<div className="flex flex-row">
|
| 65 |
+
<header>
|
| 66 |
+
<nav className="w-full bg-white text-black items-center nav-shadow fixed z-10">
|
| 67 |
+
<div className="flex flex-row justify-between">
|
| 68 |
+
<div className="flex items-center">
|
| 69 |
+
<img
|
| 70 |
+
src="/logo.svg"
|
| 71 |
+
alt="ByteRiot Logo"
|
| 72 |
+
width={200}
|
| 73 |
+
height={50}
|
| 74 |
+
className=""
|
| 75 |
+
/>
|
| 76 |
+
</div>
|
| 77 |
+
<div className="flex flex-row gap-4 items-center space-x-4">
|
| 78 |
+
<svg
|
| 79 |
+
width="24"
|
| 80 |
+
height="24"
|
| 81 |
+
viewBox="0 0 24 24"
|
| 82 |
+
fill="none"
|
| 83 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 84 |
+
>
|
| 85 |
+
<path
|
| 86 |
+
d="M12 21.759C13.1 21.759 14 20.859 14 19.759H10C10 20.859 10.9 21.759 12 21.759ZM18 15.759V10.759C18 7.68903 16.37 5.11903 13.5 4.43903V3.75903C13.5 2.92903 12.83 2.25903 12 2.25903C11.17 2.25903 10.5 2.92903 10.5 3.75903V4.43903C7.64 5.11903 6 7.67903 6 10.759V15.759L4 17.759V18.759H20V17.759L18 15.759ZM16 16.759H8V10.759C8 8.27903 9.51 6.25903 12 6.25903C14.49 6.25903 16 8.27903 16 10.759V16.759Z"
|
| 87 |
+
fill="#01A3FF"
|
| 88 |
+
/>
|
| 89 |
+
</svg>
|
| 90 |
+
<img src="/avatar.png" width={50} height={50} />
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
</nav>
|
| 94 |
+
</header>
|
| 95 |
+
<main className="pt-[75px] flex flex-row max-h-screen overflow-scroll w-full">
|
| 96 |
+
<div className="flex flex-col items-center justify-start w-[360px] px-8 gap-6 overflow-scroll border-r-1 border-r-[#DFE3E8]">
|
| 97 |
+
<h1 className="leading-8 font-bold text-[24px] py-6 text-[#01A3FF]">
|
| 98 |
+
Course Content
|
| 99 |
+
</h1>
|
| 100 |
+
<div className="border-1 w-full"></div>
|
| 101 |
+
<CourseContentAccordion
|
| 102 |
+
competencies={roadmaps?.roadmaps.roadmap_1.competencies ?? []}
|
| 103 |
+
/>
|
| 104 |
+
</div>
|
| 105 |
+
{children}
|
| 106 |
+
</main>
|
| 107 |
+
</div>
|
| 108 |
+
);
|
| 109 |
+
}
|
src/app/learning/page.tsx
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import React, { useEffect, useState } from "react";
|
| 4 |
+
import { RoadmapsRoot } from "../onboarding/learningPath";
|
| 5 |
+
import Button from "@/components/button";
|
| 6 |
+
|
| 7 |
+
function Breadcrumbs({ path }: { path: string }) {
|
| 8 |
+
const crumbs = path.split("/");
|
| 9 |
+
return (
|
| 10 |
+
<>
|
| 11 |
+
{crumbs.map((c) => (
|
| 12 |
+
<div key={c} className="flex flex-row items-center gap-2">
|
| 13 |
+
<div className="text-[14px] items-center justify-center w-fit h-fit">
|
| 14 |
+
{c}
|
| 15 |
+
</div>
|
| 16 |
+
<div>/</div>
|
| 17 |
+
</div>
|
| 18 |
+
))}
|
| 19 |
+
</>
|
| 20 |
+
);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export default function Learning({}) {
|
| 24 |
+
const [roadmaps, setRoadmaps] = useState<RoadmapsRoot | null>();
|
| 25 |
+
useEffect(() => {
|
| 26 |
+
const data = localStorage.getItem("roadmap");
|
| 27 |
+
if (data) setRoadmaps(JSON.parse(data));
|
| 28 |
+
}, []);
|
| 29 |
+
|
| 30 |
+
return (
|
| 31 |
+
<div className="flex flex-col h-full overflow-scroll px-8 gap-6 pb-2 bg-[#FAFAFA] w-full flex-1">
|
| 32 |
+
<div className="py-8 gap-2 flex flex-row items-center">
|
| 33 |
+
<Breadcrumbs path="Dashboard/Stage1/Course1" />
|
| 34 |
+
</div>
|
| 35 |
+
<div className="flex flex-col bg-white border-1 border-[#DFE3E8] gap-4 p-6 rounded-[8px]">
|
| 36 |
+
<h2 className="leading-6 text-[16px] font-bold">Your Progress</h2>
|
| 37 |
+
<div className="flex flex-row gap-4 items-center">
|
| 38 |
+
<div className="flex flex-row gap-3 items-center">
|
| 39 |
+
<img src={"/duration.svg"} width={24} height={24} />
|
| 40 |
+
<p className="text-[14px] leading-5 text-[#637381]">
|
| 41 |
+
Duration: 48 weeks
|
| 42 |
+
</p>
|
| 43 |
+
</div>
|
| 44 |
+
<div className="flex flex-row gap-3 items-center">
|
| 45 |
+
<img src={"/video.svg"} width={24} height={24} />
|
| 46 |
+
<p className="text-[14px] leading-5 text-[#637381]">
|
| 47 |
+
12 Learning video
|
| 48 |
+
</p>
|
| 49 |
+
</div>
|
| 50 |
+
<div className="flex flex-row gap-3 items-center">
|
| 51 |
+
<img src={"/description.svg"} width={24} height={24} />
|
| 52 |
+
<p className="text-[14px] leading-5 text-[#637381]">8 courses</p>
|
| 53 |
+
</div>
|
| 54 |
+
<div className="flex flex-row gap-3 items-center">
|
| 55 |
+
<img src={"/book.svg"} width={24} height={24} />
|
| 56 |
+
<p className="text-[14px] leading-5 text-[#637381]">5 books</p>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
<div className="w-full h-fit">
|
| 61 |
+
<iframe
|
| 62 |
+
className="min-h-[512px]"
|
| 63 |
+
width="100%"
|
| 64 |
+
height="100%"
|
| 65 |
+
src="https://www.youtube.com/embed/XU5pw3QRYjQ?si=oJdWTisd-04KyDAC"
|
| 66 |
+
title="YouTube video player"
|
| 67 |
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
| 68 |
+
referrerPolicy="strict-origin-when-cross-origin"
|
| 69 |
+
allowFullScreen
|
| 70 |
+
></iframe>
|
| 71 |
+
</div>
|
| 72 |
+
<div className="flex flex-col border-1 border-[#DFE3E8] gap-4 p-6 rounded-[8px]">
|
| 73 |
+
<div className="flex flex-row items-center">
|
| 74 |
+
<div className="flex w-fit px-3">
|
| 75 |
+
<p className="font-bold h-full items-center justify-center border-b-1 py-3 border-b-[#01A3FF] w-fit">
|
| 76 |
+
My Notes
|
| 77 |
+
</p>
|
| 78 |
+
</div>
|
| 79 |
+
<div className="flex w-fit px-3">
|
| 80 |
+
<p className="h-full items-center justify-center w-fit text-[#637381]">
|
| 81 |
+
Resources & Downloads
|
| 82 |
+
</p>
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
|
| 86 |
+
<div className="w-full border-1"></div>
|
| 87 |
+
|
| 88 |
+
<div className="flex flex-col gap-2.5">
|
| 89 |
+
<h2 className="leading-6 text-[16px] font-bold">Important Notes</h2>
|
| 90 |
+
<textarea
|
| 91 |
+
className="w-full min-h-[100px] bg-white border-1 border-[#DFE3E8] rounded-[8px] p-2 text-[14px]"
|
| 92 |
+
placeholder="Write down your notes here"
|
| 93 |
+
/>
|
| 94 |
+
<Button
|
| 95 |
+
label="Save Notes"
|
| 96 |
+
className="w-fit self-end"
|
| 97 |
+
onClick={() => {}}
|
| 98 |
+
/>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
);
|
| 103 |
+
}
|
src/app/onboarding/layout.tsx
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default function OnboardingLayout({
|
| 2 |
+
children,
|
| 3 |
+
}: Readonly<{
|
| 4 |
+
children: React.ReactNode;
|
| 5 |
+
}>) {
|
| 6 |
+
return (
|
| 7 |
+
<div className="font-sans flex flex-col min-h-screen">
|
| 8 |
+
<header>
|
| 9 |
+
<nav className="w-full bg-white text-black items-center nav-shadow fixed z-10">
|
| 10 |
+
<div className="flex flex-row justify-between">
|
| 11 |
+
<div className="flex items-center">
|
| 12 |
+
<img
|
| 13 |
+
src="/logo.svg"
|
| 14 |
+
alt="ByteRiot Logo"
|
| 15 |
+
width={200}
|
| 16 |
+
height={50}
|
| 17 |
+
className=""
|
| 18 |
+
/>
|
| 19 |
+
</div>
|
| 20 |
+
<div className="flex flex-row gap-4 items-center space-x-4">
|
| 21 |
+
<svg
|
| 22 |
+
width="24"
|
| 23 |
+
height="24"
|
| 24 |
+
viewBox="0 0 24 24"
|
| 25 |
+
fill="none"
|
| 26 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 27 |
+
>
|
| 28 |
+
<path
|
| 29 |
+
d="M12 21.759C13.1 21.759 14 20.859 14 19.759H10C10 20.859 10.9 21.759 12 21.759ZM18 15.759V10.759C18 7.68903 16.37 5.11903 13.5 4.43903V3.75903C13.5 2.92903 12.83 2.25903 12 2.25903C11.17 2.25903 10.5 2.92903 10.5 3.75903V4.43903C7.64 5.11903 6 7.67903 6 10.759V15.759L4 17.759V18.759H20V17.759L18 15.759ZM16 16.759H8V10.759C8 8.27903 9.51 6.25903 12 6.25903C14.49 6.25903 16 8.27903 16 10.759V16.759Z"
|
| 30 |
+
fill="#01A3FF"
|
| 31 |
+
/>
|
| 32 |
+
</svg>
|
| 33 |
+
<img src="/avatar.png" width={50} height={50} />
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
</nav>
|
| 37 |
+
</header>
|
| 38 |
+
{children}
|
| 39 |
+
<footer className="w-full bg-white text-black items-center nav-shadow">
|
| 40 |
+
<div className="flex flex-row justify-between p-4">
|
| 41 |
+
<span className="text-sm">© 2025 ByteRiot</span>
|
| 42 |
+
</div>
|
| 43 |
+
</footer>
|
| 44 |
+
</div>
|
| 45 |
+
);
|
| 46 |
+
}
|
src/app/onboarding/learningPath.tsx
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from "react";
|
| 2 |
+
import Loading from "./loading";
|
| 3 |
+
import Button from "@/components/button";
|
| 4 |
+
import { ResumeData } from "./upload";
|
| 5 |
+
import {
|
| 6 |
+
Accordion,
|
| 7 |
+
AccordionContent,
|
| 8 |
+
AccordionItem,
|
| 9 |
+
AccordionTrigger,
|
| 10 |
+
} from "@/components/ui/accordion";
|
| 11 |
+
|
| 12 |
+
export interface RoadmapsRoot {
|
| 13 |
+
roadmaps: Roadmaps;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export interface Roadmaps {
|
| 17 |
+
roadmap_1: Roadmap1;
|
| 18 |
+
roadmap_2: Roadmap2;
|
| 19 |
+
roadmap_3: Roadmap3;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export interface Roadmap1 {
|
| 23 |
+
competencies: Competency[];
|
| 24 |
+
insight: string;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export interface Competency {
|
| 28 |
+
skill: string;
|
| 29 |
+
description: string;
|
| 30 |
+
resources: string[];
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export interface Roadmap2 {
|
| 34 |
+
competencies: Competency2[];
|
| 35 |
+
insight: string;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
export interface Competency2 {
|
| 39 |
+
skill: string;
|
| 40 |
+
description: string;
|
| 41 |
+
resources: string[];
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
export interface Roadmap3 {
|
| 45 |
+
competencies: Competency3[];
|
| 46 |
+
insight: string;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
export interface Competency3 {
|
| 50 |
+
skill: string;
|
| 51 |
+
description: string;
|
| 52 |
+
resources: string[];
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
function LearningAccordion({
|
| 56 |
+
competencies,
|
| 57 |
+
}: {
|
| 58 |
+
competencies: { skill: string; description: string; resources: string[] }[];
|
| 59 |
+
}) {
|
| 60 |
+
return (
|
| 61 |
+
<div className="flex flex-col w-full gap-6">
|
| 62 |
+
{competencies.map((c, idx) => (
|
| 63 |
+
<Accordion
|
| 64 |
+
type="single"
|
| 65 |
+
collapsible
|
| 66 |
+
className="w-full border-1 border-[#DFE3E8] rounded-[8px] p-6"
|
| 67 |
+
key={c.skill}
|
| 68 |
+
>
|
| 69 |
+
<AccordionItem value={c.skill}>
|
| 70 |
+
<AccordionTrigger className="focus:cursor-pointer p-0">
|
| 71 |
+
<div className="flex flex-row items-start gap-4">
|
| 72 |
+
<div className="flex w-[40px] p-2 gap-2-5 items-center justify-center rounded-3xl bg-[#D5EBFF]">
|
| 73 |
+
<p className="leading-6 font-bold text-[16px] text-[#01A3FF]">
|
| 74 |
+
{idx + 1}
|
| 75 |
+
</p>
|
| 76 |
+
</div>
|
| 77 |
+
<div className="flex flex-col gap-2.5">
|
| 78 |
+
<h2 className="text-[16px] font-bold leading-6">{c.skill}</h2>
|
| 79 |
+
<p className="text-[#637381] text-[14px] leading-5">
|
| 80 |
+
{c.description}
|
| 81 |
+
</p>
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
</AccordionTrigger>
|
| 85 |
+
<AccordionContent className="flex flex-col gap-4 text-balance">
|
| 86 |
+
<div className="w-full border-1 my-4"></div>
|
| 87 |
+
<div className="flex flex-col w-full gap-4">
|
| 88 |
+
<h2 className="text-[16px] leading-6 font-bold">
|
| 89 |
+
Key Topics & Resources:
|
| 90 |
+
</h2>
|
| 91 |
+
{c.resources.map((c, idx) => (
|
| 92 |
+
<p
|
| 93 |
+
className="text-[#637381] text-[14px] leading-5"
|
| 94 |
+
key={`${c}-${idx}`}
|
| 95 |
+
>
|
| 96 |
+
{c}
|
| 97 |
+
</p>
|
| 98 |
+
))}
|
| 99 |
+
</div>
|
| 100 |
+
</AccordionContent>
|
| 101 |
+
</AccordionItem>
|
| 102 |
+
</Accordion>
|
| 103 |
+
))}
|
| 104 |
+
</div>
|
| 105 |
+
);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
export default function LearningPath({
|
| 109 |
+
onNext,
|
| 110 |
+
userProfile,
|
| 111 |
+
}: {
|
| 112 |
+
onNext: () => void;
|
| 113 |
+
userProfile: ResumeData | null;
|
| 114 |
+
}) {
|
| 115 |
+
const [isLoading, setIsLoading] = useState<boolean>(false);
|
| 116 |
+
const [resumeData, setResumeData] = useState<null>(null);
|
| 117 |
+
const [progress, setProgress] = useState<number>(0);
|
| 118 |
+
const [textValue, setTextValue] = useState("");
|
| 119 |
+
const [roadmap, setRoadmap] = useState<RoadmapsRoot>();
|
| 120 |
+
|
| 121 |
+
if (isLoading)
|
| 122 |
+
return (
|
| 123 |
+
<Loading
|
| 124 |
+
title="Our AI is making a secret magic formula"
|
| 125 |
+
desc="Crafting the best learning path for your career..."
|
| 126 |
+
progress={progress}
|
| 127 |
+
imgSrc="/loading2.png"
|
| 128 |
+
/>
|
| 129 |
+
);
|
| 130 |
+
|
| 131 |
+
const onContinue = async () => {
|
| 132 |
+
setIsLoading(true);
|
| 133 |
+
try {
|
| 134 |
+
const body = {
|
| 135 |
+
user_profile: userProfile,
|
| 136 |
+
user_target: textValue,
|
| 137 |
+
};
|
| 138 |
+
setProgress(10);
|
| 139 |
+
const response = await fetch("http://localhost:8000/roadmaps", {
|
| 140 |
+
method: "POST",
|
| 141 |
+
headers: {
|
| 142 |
+
"Content-Type": "application/json",
|
| 143 |
+
},
|
| 144 |
+
body: JSON.stringify(body),
|
| 145 |
+
});
|
| 146 |
+
|
| 147 |
+
if (!response.body) return;
|
| 148 |
+
|
| 149 |
+
console.log(response.body);
|
| 150 |
+
setProgress(70);
|
| 151 |
+
const roadmaps = await response.json();
|
| 152 |
+
setRoadmap(roadmaps);
|
| 153 |
+
localStorage.setItem("roadmap", JSON.stringify(roadmaps));
|
| 154 |
+
setProgress(80);
|
| 155 |
+
} catch (error) {
|
| 156 |
+
console.error("Processing error:", error);
|
| 157 |
+
}
|
| 158 |
+
setIsLoading(false);
|
| 159 |
+
setProgress(100);
|
| 160 |
+
};
|
| 161 |
+
|
| 162 |
+
if (roadmap)
|
| 163 |
+
return (
|
| 164 |
+
<div className="flex flex-col items-center justify-center gap-4 p-6 w-[720px]">
|
| 165 |
+
<div className="flex">
|
| 166 |
+
<h1 className="text-2xl font-bold leading-8">
|
| 167 |
+
Recommended Learning Path
|
| 168 |
+
</h1>
|
| 169 |
+
</div>
|
| 170 |
+
<div className="p-6 gap-2.5 flex flex-col items-start bg-[#EAF5FF] border-l-4 border-l-[#01A3FF]">
|
| 171 |
+
<h2 className="text-[16px] leading-5 font-bold">Why this path?</h2>
|
| 172 |
+
<p className="text-[16px] leading-5">
|
| 173 |
+
{roadmap.roadmaps.roadmap_1.insight}
|
| 174 |
+
</p>
|
| 175 |
+
</div>
|
| 176 |
+
<LearningAccordion
|
| 177 |
+
competencies={roadmap.roadmaps.roadmap_1.competencies}
|
| 178 |
+
/>
|
| 179 |
+
<a href="/learning">
|
| 180 |
+
<Button
|
| 181 |
+
label="Start Learning"
|
| 182 |
+
onClick={onNext}
|
| 183 |
+
className="self-end"
|
| 184 |
+
/>
|
| 185 |
+
</a>
|
| 186 |
+
</div>
|
| 187 |
+
);
|
| 188 |
+
|
| 189 |
+
return (
|
| 190 |
+
<div className="flex flex-col gap-2.5 items-center justify-center">
|
| 191 |
+
<div className="flex flex-col justify-center items-center p-6 gap-2.5">
|
| 192 |
+
<h1 className="text-2xl font-bold leading-8">
|
| 193 |
+
Describe your target or goals:
|
| 194 |
+
</h1>
|
| 195 |
+
<p className="text-[18px] leading-[28px]">
|
| 196 |
+
Write down your specific goals or career target for the future.
|
| 197 |
+
</p>
|
| 198 |
+
</div>
|
| 199 |
+
<textarea
|
| 200 |
+
onChange={(e) => setTextValue(e.target.value)}
|
| 201 |
+
className="p-6 w-[720px] h-[158px] border-1 rounded-[8px] border-[#DFE3E8] focus:outline-[1px] focus:outline-[#DFE3E8]"
|
| 202 |
+
></textarea>
|
| 203 |
+
<Button label="Continue" onClick={onContinue} className="self-end" />
|
| 204 |
+
</div>
|
| 205 |
+
);
|
| 206 |
+
}
|
src/app/onboarding/loading.tsx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Image from "next/image";
|
| 2 |
+
|
| 3 |
+
type LoadingProps = {
|
| 4 |
+
title: string;
|
| 5 |
+
desc: string;
|
| 6 |
+
imgSrc: string;
|
| 7 |
+
progress: number;
|
| 8 |
+
};
|
| 9 |
+
|
| 10 |
+
const LoadingBar = ({ progress }: { progress: number }) => {
|
| 11 |
+
// Ensure the progress value is between 0 and 100
|
| 12 |
+
const clampedProgress = Math.max(0, Math.min(100, progress));
|
| 13 |
+
|
| 14 |
+
return (
|
| 15 |
+
<div className="flex flex-row w-full items-center gap-2">
|
| 16 |
+
<div className="w-full bg-[#E6F6FF] rounded-sm h-1 overflow-hidden">
|
| 17 |
+
<div
|
| 18 |
+
className="h-full bg-[#01A3FF] rounded-sm transition-[width] ease-in-out duration-300"
|
| 19 |
+
style={{ width: `${clampedProgress}%` }}
|
| 20 |
+
></div>
|
| 21 |
+
</div>
|
| 22 |
+
<p>{clampedProgress}%</p>
|
| 23 |
+
</div>
|
| 24 |
+
);
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
export default function Loading(props: LoadingProps) {
|
| 28 |
+
return (
|
| 29 |
+
<div className="flex flex-col items-center justify-center gap-2.5 w-full">
|
| 30 |
+
<h1 className="py-6 text-[32px]">{props.title}</h1>
|
| 31 |
+
<Image alt="Loading Image" src={props.imgSrc} width={360} height={360} />
|
| 32 |
+
<div className="flex flex-col lg:w-[70%] w-[90%] gap-2.5 items-center justify-center">
|
| 33 |
+
<p className="font-normal text-[14px] text-[#637381]">{props.desc}</p>
|
| 34 |
+
<LoadingBar progress={props.progress} />
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
);
|
| 38 |
+
}
|
src/app/onboarding/page.tsx
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useState } from "react";
|
| 4 |
+
import Page1 from "./page1";
|
| 5 |
+
import UploadCV, { ResumeData } from "./upload";
|
| 6 |
+
import LearningPath from "./learningPath";
|
| 7 |
+
|
| 8 |
+
export default function Onboarding() {
|
| 9 |
+
const [step, setStep] = useState(1);
|
| 10 |
+
const [resumeData, setResumeData] = useState<ResumeData | null>(null);
|
| 11 |
+
|
| 12 |
+
return (
|
| 13 |
+
<main className="flex flex-col items-center justify-center pt-[75px] text-black min-h-screen">
|
| 14 |
+
{step === 1 && <Page1 onChoose={() => setStep(2)} />}
|
| 15 |
+
{step === 2 && <UploadCV onNext={() => setStep(3)} resumeData={resumeData} setResumeData={setResumeData} />}
|
| 16 |
+
{step === 3 && <LearningPath onNext={() => {}} userProfile={resumeData} />}
|
| 17 |
+
</main>
|
| 18 |
+
);
|
| 19 |
+
}
|
src/app/onboarding/page1.tsx
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Button from "@/components/button";
|
| 2 |
+
|
| 3 |
+
type Props = {
|
| 4 |
+
onChoose?: () => void;
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export default function Page1({onChoose}: Props) {
|
| 8 |
+
const cards = [
|
| 9 |
+
{
|
| 10 |
+
title: "Upload CV",
|
| 11 |
+
description:
|
| 12 |
+
"The fastest option if you have an up-to-date CV. Let our AI analyze it automatically.",
|
| 13 |
+
imageUrl: "/doc.svg",
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
title: "Use a Text Prompt",
|
| 17 |
+
description:
|
| 18 |
+
"Describe your goals and background in your own words. Ideal if you know exactly what you want.",
|
| 19 |
+
imageUrl: "/paragraph.svg",
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
title: "Take a Guided Questionnaire",
|
| 23 |
+
description:
|
| 24 |
+
"Not sure where to begin? We'll guide you with a few short questions.",
|
| 25 |
+
imageUrl: "/chat.svg",
|
| 26 |
+
},
|
| 27 |
+
];
|
| 28 |
+
|
| 29 |
+
return (
|
| 30 |
+
<>
|
| 31 |
+
<div className="mb-8">
|
| 32 |
+
<img src="/robot-1.png" alt="Logo" className="max-w-full h-auto" />
|
| 33 |
+
</div>
|
| 34 |
+
<div className="font-sans flex flex-col gap-2.5 items-center justify-center">
|
| 35 |
+
<h1 className="text-[32px]">How would you like to start?</h1>
|
| 36 |
+
<p>
|
| 37 |
+
Our AI needs to understand you to create the best recommendation.
|
| 38 |
+
Choose the method that's most convenient for you.
|
| 39 |
+
</p>
|
| 40 |
+
</div>
|
| 41 |
+
<div className="grid grid-cols-3 gap-8 mt-8">
|
| 42 |
+
{cards.map((card) => (
|
| 43 |
+
<Card key={card.title} {...card} classname="max-w-[360px]" onChoose={onChoose} />
|
| 44 |
+
))}
|
| 45 |
+
</div>
|
| 46 |
+
</>
|
| 47 |
+
);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
type CardProps = {
|
| 51 |
+
title: string;
|
| 52 |
+
description: string;
|
| 53 |
+
imageUrl: string;
|
| 54 |
+
classname?: string;
|
| 55 |
+
onChoose?: () => void;
|
| 56 |
+
};
|
| 57 |
+
|
| 58 |
+
function Card({ title, description, imageUrl, classname, onChoose = () => {} }: CardProps) {
|
| 59 |
+
return (
|
| 60 |
+
<div
|
| 61 |
+
className={`flex flex-col items-center justify-between p-4 border-1 rounded-[8px] border-[#DFE3E8] ${classname}`}
|
| 62 |
+
>
|
| 63 |
+
<div className="flex flex-col items-center justify-center gap-3">
|
| 64 |
+
<div className="flex items-center w-full justify-center px-8 py-4 rounded-[8px] border-1 border-[#DFE3E8] bg-[#EAF5FF]">
|
| 65 |
+
<img src={imageUrl} alt={title} className="w-[82px] h-[82px]" />
|
| 66 |
+
</div>
|
| 67 |
+
<div className="flex flex-col items-start text-left gap-4">
|
| 68 |
+
<h1 className="text-[16px] font-semibold">{title}</h1>
|
| 69 |
+
<p className="text-[14px] text-gray-600 text-justify">
|
| 70 |
+
{description}
|
| 71 |
+
</p>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
<div className="flex items-center w-full self-end">
|
| 75 |
+
<Button onClick={() => onChoose()} label="Choose" buttonType="primary">
|
| 76 |
+
</Button>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
);
|
| 80 |
+
}
|
src/app/onboarding/upload.tsx
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
import Button from "@/components/button";
|
| 3 |
+
import {
|
| 4 |
+
Dropzone,
|
| 5 |
+
DropZoneArea,
|
| 6 |
+
DropzoneDescription,
|
| 7 |
+
DropzoneFileList,
|
| 8 |
+
DropzoneFileListItem,
|
| 9 |
+
DropzoneMessage,
|
| 10 |
+
DropzoneRemoveFile,
|
| 11 |
+
DropzoneTrigger,
|
| 12 |
+
useDropzone,
|
| 13 |
+
UseDropzoneReturn,
|
| 14 |
+
} from "@/components/ui/dropzone";
|
| 15 |
+
import { CloudUploadIcon, Trash2Icon } from "lucide-react";
|
| 16 |
+
import { useState } from "react";
|
| 17 |
+
import Loading from "./loading";
|
| 18 |
+
|
| 19 |
+
export interface ResumeData {
|
| 20 |
+
personal_info: PersonalInfo;
|
| 21 |
+
summary: string;
|
| 22 |
+
yoe: number;
|
| 23 |
+
work_experience: WorkExperience[];
|
| 24 |
+
education: Education[];
|
| 25 |
+
skills: Skills;
|
| 26 |
+
projects: Project[];
|
| 27 |
+
certifications: Certification[];
|
| 28 |
+
languages: Language[];
|
| 29 |
+
awards_honors: AwardsHonor[];
|
| 30 |
+
publications: any[];
|
| 31 |
+
interests: any[];
|
| 32 |
+
character_traits_keywords: string[];
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
interface WorkExperience {
|
| 36 |
+
company_name: string;
|
| 37 |
+
job_title: string;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
export interface PersonalInfo {
|
| 41 |
+
full_name: string;
|
| 42 |
+
email: string;
|
| 43 |
+
phone: string;
|
| 44 |
+
linkedin_profile: string;
|
| 45 |
+
portfolio_url: string;
|
| 46 |
+
address: Address;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
export interface Address {
|
| 50 |
+
street: any;
|
| 51 |
+
city: any;
|
| 52 |
+
state: any;
|
| 53 |
+
zip_code: any;
|
| 54 |
+
country: any;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
export interface Education {
|
| 58 |
+
degree: string;
|
| 59 |
+
major: string;
|
| 60 |
+
institution: string;
|
| 61 |
+
location: string;
|
| 62 |
+
start_date: string;
|
| 63 |
+
end_date: string;
|
| 64 |
+
gpa: string;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
export interface Skills {
|
| 68 |
+
technical_skills: string[];
|
| 69 |
+
soft_skills: string[];
|
| 70 |
+
tools_technologies: any[];
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
export interface Project {
|
| 74 |
+
project_name: string;
|
| 75 |
+
description: string;
|
| 76 |
+
technologies_used: string[];
|
| 77 |
+
project_url: any;
|
| 78 |
+
start_date?: string;
|
| 79 |
+
end_date?: string;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
export interface Certification {
|
| 83 |
+
name: string;
|
| 84 |
+
issuing_organization: string;
|
| 85 |
+
issue_date: string;
|
| 86 |
+
expiration_date: any;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
export interface Language {
|
| 90 |
+
language: string;
|
| 91 |
+
proficiency: string;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
export interface AwardsHonor {
|
| 95 |
+
name: string;
|
| 96 |
+
issuing_organization: string;
|
| 97 |
+
date: string;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
export function CVDropzone({
|
| 101 |
+
dropzone,
|
| 102 |
+
}: {
|
| 103 |
+
dropzone: UseDropzoneReturn<string, string>;
|
| 104 |
+
}) {
|
| 105 |
+
return (
|
| 106 |
+
<div className="not-prose flex flex-col gap-4">
|
| 107 |
+
<Dropzone {...dropzone}>
|
| 108 |
+
<div>
|
| 109 |
+
<div className="flex justify-between min-h-0">
|
| 110 |
+
<DropzoneMessage />
|
| 111 |
+
</div>
|
| 112 |
+
<DropZoneArea className="min-h-[300px] border-1 border-dashed border-[#57ADFF] rounded-[8px] bg-[#EAF5FF] hover:bg-[#EAF5FF] h-full">
|
| 113 |
+
<DropzoneTrigger className="flex flex-col items-center gap-4 bg-transparent p-10 text-center text-sm h-full hover:bg-[#EAF5FF]">
|
| 114 |
+
<CloudUploadIcon className="size-8" />
|
| 115 |
+
<div>
|
| 116 |
+
<p className="text-[16px] font-bold leading-6">
|
| 117 |
+
Drag & drop files or click to browse
|
| 118 |
+
</p>
|
| 119 |
+
<p className="text-sm text-[#676767] font-normal">
|
| 120 |
+
Click here or drag and drop to upload
|
| 121 |
+
</p>
|
| 122 |
+
</div>
|
| 123 |
+
</DropzoneTrigger>
|
| 124 |
+
</DropZoneArea>
|
| 125 |
+
</div>
|
| 126 |
+
|
| 127 |
+
<DropzoneFileList className="grid gap-3 p-0 grid-cols-1 w-full">
|
| 128 |
+
{dropzone.fileStatuses.map((file) => (
|
| 129 |
+
<DropzoneFileListItem
|
| 130 |
+
className="overflow-hidden rounded-md bg-secondary p-0 shadow-sm w-full"
|
| 131 |
+
key={file.id}
|
| 132 |
+
file={file}
|
| 133 |
+
>
|
| 134 |
+
{file.status === "pending" ? (
|
| 135 |
+
<div className="flex items-center w-full justify-between p-2 pl-4 animate-pulse" />
|
| 136 |
+
) : (
|
| 137 |
+
<div className="flex items-center w-full justify-between p-2 pl-4">
|
| 138 |
+
<div className="min-w-0">
|
| 139 |
+
<p className="truncate text-sm">{file.fileName}</p>
|
| 140 |
+
<p className="text-xs text-muted-foreground">
|
| 141 |
+
{(file.file.size / (1024 * 1024)).toFixed(2)} MB
|
| 142 |
+
</p>
|
| 143 |
+
</div>
|
| 144 |
+
<DropzoneRemoveFile
|
| 145 |
+
variant="destructive"
|
| 146 |
+
className="shrink-0 hover:outline"
|
| 147 |
+
>
|
| 148 |
+
<Trash2Icon className="size-4" />
|
| 149 |
+
</DropzoneRemoveFile>
|
| 150 |
+
</div>
|
| 151 |
+
)}
|
| 152 |
+
</DropzoneFileListItem>
|
| 153 |
+
))}
|
| 154 |
+
</DropzoneFileList>
|
| 155 |
+
</Dropzone>
|
| 156 |
+
</div>
|
| 157 |
+
);
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
export default function UploadCV({
|
| 161 |
+
onNext,
|
| 162 |
+
resumeData,
|
| 163 |
+
setResumeData
|
| 164 |
+
}: {
|
| 165 |
+
onNext: () => void;
|
| 166 |
+
resumeData: ResumeData | null;
|
| 167 |
+
setResumeData: React.Dispatch<React.SetStateAction<ResumeData | null>>;
|
| 168 |
+
}) {
|
| 169 |
+
const [isUploading, setIsUploading] = useState<boolean>(false);
|
| 170 |
+
const [progress, setProgress] = useState<number>(0);
|
| 171 |
+
|
| 172 |
+
const dropzone = useDropzone({
|
| 173 |
+
onDropFile: async (file: File) => {
|
| 174 |
+
await new Promise((resolve) => setTimeout(resolve, 700));
|
| 175 |
+
return {
|
| 176 |
+
status: "success",
|
| 177 |
+
result: URL.createObjectURL(file),
|
| 178 |
+
};
|
| 179 |
+
},
|
| 180 |
+
validation: {
|
| 181 |
+
accept: {
|
| 182 |
+
"application/*": [".pdf", ".docx", ".txt"],
|
| 183 |
+
},
|
| 184 |
+
maxSize: 5 * 1024 * 1024,
|
| 185 |
+
maxFiles: 1,
|
| 186 |
+
},
|
| 187 |
+
});
|
| 188 |
+
|
| 189 |
+
const extractJsonFromString = (text: string) => {
|
| 190 |
+
const match = text.match(/```json\s*([\s\S]*?)\s*```/);
|
| 191 |
+
// The captured group (in parentheses) is the JSON content itself
|
| 192 |
+
return match ? match[1] : null;
|
| 193 |
+
};
|
| 194 |
+
|
| 195 |
+
const uploadFile = async () => {
|
| 196 |
+
setIsUploading(true);
|
| 197 |
+
let fullResponse = "";
|
| 198 |
+
for (let i = 0; i < 5; i++) {
|
| 199 |
+
console.log(`try number ${i}`);
|
| 200 |
+
try {
|
| 201 |
+
const response = await fetch("http://localhost:8000/autograder", {
|
| 202 |
+
method: "POST",
|
| 203 |
+
headers: {
|
| 204 |
+
"Content-Type": "application/octet-stream",
|
| 205 |
+
},
|
| 206 |
+
body: dropzone.fileStatuses[0].file,
|
| 207 |
+
});
|
| 208 |
+
|
| 209 |
+
setProgress(10);
|
| 210 |
+
|
| 211 |
+
if (!response.body) return;
|
| 212 |
+
|
| 213 |
+
const reader = response.body.getReader();
|
| 214 |
+
const decoder = new TextDecoder("utf-8");
|
| 215 |
+
|
| 216 |
+
setProgress(70);
|
| 217 |
+
while (true) {
|
| 218 |
+
const { done, value } = await reader.read();
|
| 219 |
+
if (done) break;
|
| 220 |
+
|
| 221 |
+
const chunk = decoder.decode(value, { stream: true });
|
| 222 |
+
fullResponse += chunk;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
// The crucial step: parse the extracted JSON string
|
| 226 |
+
let resumeData = JSON.parse(extractJsonFromString(fullResponse) || "");
|
| 227 |
+
console.log(resumeData);
|
| 228 |
+
setResumeData(resumeData);
|
| 229 |
+
setProgress(80);
|
| 230 |
+
break;
|
| 231 |
+
} catch (error) {
|
| 232 |
+
console.log(extractJsonFromString(fullResponse));
|
| 233 |
+
console.error("Processing error:", error);
|
| 234 |
+
continue;
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
setIsUploading(false);
|
| 238 |
+
setProgress(100);
|
| 239 |
+
};
|
| 240 |
+
|
| 241 |
+
if (resumeData) {
|
| 242 |
+
return (
|
| 243 |
+
<div className="flex flex-col p-6">
|
| 244 |
+
<h1 className="text-2xl font-bold py-6 my-2.5 leading-8">
|
| 245 |
+
Does this look right? We found these key points in your CV:
|
| 246 |
+
</h1>
|
| 247 |
+
<div className="grid grid-cols-[120px_500px] grid-rows-[auto_auto_auto_auto_auto] p-6 rounded-[8px] gap-2.5 text-[14px] leading-[20px] border-1 border-[#DFE3E8]">
|
| 248 |
+
<p className="font-bold">Name:</p>
|
| 249 |
+
<p>{resumeData?.personal_info.full_name}</p>
|
| 250 |
+
<p className="font-bold">Last Position:</p>
|
| 251 |
+
<p>
|
| 252 |
+
{resumeData?.work_experience.length === 0
|
| 253 |
+
? "Fresh Graduate"
|
| 254 |
+
: `${resumeData?.work_experience[0].job_title}, ${resumeData?.work_experience[0].company_name}`}
|
| 255 |
+
</p>
|
| 256 |
+
<p className="font-bold">Years of Experience:</p>
|
| 257 |
+
<p>
|
| 258 |
+
{resumeData.yoe}
|
| 259 |
+
</p>
|
| 260 |
+
<p className="font-bold">Key Skills:</p>
|
| 261 |
+
<div className="flex flex-row flex-wrap gap-2">
|
| 262 |
+
{resumeData?.skills.technical_skills.map(skill => (
|
| 263 |
+
<p key={skill} className="bg-[#7765e3] rounded-[8px] p-1 text-white leading-5 font-medium">
|
| 264 |
+
{skill}
|
| 265 |
+
</p>
|
| 266 |
+
))}
|
| 267 |
+
{resumeData?.skills.soft_skills.map(skill => (
|
| 268 |
+
<p key={skill} className="bg-[#dcbba9] rounded-[8px] p-1 text-white leading-5 font-medium">
|
| 269 |
+
{skill}
|
| 270 |
+
</p>
|
| 271 |
+
))}
|
| 272 |
+
</div>
|
| 273 |
+
<p className="font-bold">Education:</p>
|
| 274 |
+
<p>
|
| 275 |
+
{resumeData?.education.length === 0
|
| 276 |
+
? ""
|
| 277 |
+
: resumeData?.education[0].degree}
|
| 278 |
+
</p>
|
| 279 |
+
</div>
|
| 280 |
+
<div className="flex flex-row w-full justify-end gap-2 items-center">
|
| 281 |
+
<Button label="Edit" buttonType="secondary" onClick={() => {}} />
|
| 282 |
+
<Button label="Yes, continue" buttonType="primary" onClick={onNext} />
|
| 283 |
+
</div>
|
| 284 |
+
</div>
|
| 285 |
+
);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
if (isUploading)
|
| 289 |
+
return (
|
| 290 |
+
<Loading
|
| 291 |
+
title="Our AI is analyzing your CV"
|
| 292 |
+
desc="Extracting your profile..."
|
| 293 |
+
progress={progress}
|
| 294 |
+
imgSrc="/loading1.png"
|
| 295 |
+
/>
|
| 296 |
+
);
|
| 297 |
+
|
| 298 |
+
return (
|
| 299 |
+
<div className="flex flex-col items-center justify-center w-full">
|
| 300 |
+
<img src="/logo.svg" alt="SkillSync logo" width={268} height={67} />
|
| 301 |
+
<h1 className="text-2xl font-bold py-6 my-2.5 leading-8">
|
| 302 |
+
Drag & drop your CV here, or click to browse files.
|
| 303 |
+
</h1>
|
| 304 |
+
<div className="flex flex-col gap-2.5 p-6 border-1 rounded-[8px] border-[#DFE3E8] w-full max-w-[445px] items-center justify-center">
|
| 305 |
+
<h1 className="text-[22px] font-bold leading-8">Upload</h1>
|
| 306 |
+
<CVDropzone dropzone={dropzone} />
|
| 307 |
+
<Button
|
| 308 |
+
className="w-full"
|
| 309 |
+
label="Upload File"
|
| 310 |
+
buttonType="primary"
|
| 311 |
+
onClick={uploadFile}
|
| 312 |
+
></Button>
|
| 313 |
+
</div>
|
| 314 |
+
</div>
|
| 315 |
+
);
|
| 316 |
+
}
|