Yvonne Priscilla commited on
Commit
e6f1924
·
1 Parent(s): 36f1e82

init commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
.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
+ }