Yvonne Priscilla commited on
Commit
bf5a50c
ยท
1 Parent(s): 66ed0c8

fix chart data based on login

Browse files
src/app/api/cv-profile/charts/major/route.ts CHANGED
@@ -2,35 +2,65 @@ import { prisma } from "@/lib/prisma";
2
  import { NextResponse } from "next/server";
3
 
4
  function randomColor() {
5
- return `#${Math.floor(Math.random() * 16777215).toString(16)}`;
 
 
6
  }
7
 
8
- export async function GET() {
9
- const profiles = await prisma.cv_profile.findMany({
10
- select: {
11
- major_edu_1: true,
12
- major_edu_2: true,
13
- major_edu_3: true,
14
- },
15
- });
16
-
17
- const majorCount: Record<string, number> = {};
18
-
19
- profiles.forEach((profile) => {
20
- [profile.major_edu_1, profile.major_edu_2, profile.major_edu_3]
21
- .filter(Boolean)
22
- .forEach((univ) => {
23
- if (!univ) return;
24
-
25
- majorCount[univ] = (majorCount[univ] || 0) + 1;
26
- });
27
- });
28
-
29
-
30
-
31
- return NextResponse.json(Object.entries(majorCount).map(([name, value]) => ({
32
- name,
33
- value,
34
- color: randomColor(),
35
- })));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  }
 
2
  import { NextResponse } from "next/server";
3
 
4
  function randomColor() {
5
+ return `#${Math.floor(Math.random() * 16777215)
6
+ .toString(16)
7
+ .padStart(6, "0")}`;
8
  }
9
 
10
+ export async function GET(req: Request) {
11
+ try {
12
+ const { searchParams } = new URL(req.url);
13
+ const user_id = searchParams.get("user_id");
14
+
15
+ if (!user_id) {
16
+ return NextResponse.json(
17
+ { message: "user_id is required" },
18
+ { status: 400 }
19
+ );
20
+ }
21
+
22
+ // โœ… RAW QUERY WITH JOIN
23
+ const profiles = await prisma.$queryRaw<
24
+ {
25
+ major_edu_1: string | null;
26
+ major_edu_2: string | null;
27
+ major_edu_3: string | null;
28
+ }[]
29
+ >`
30
+ SELECT
31
+ p.major_edu_1,
32
+ p.major_edu_2,
33
+ p.major_edu_3
34
+ FROM cv_profile p
35
+ INNER JOIN cv_file f ON f.file_id = p.file_id
36
+ WHERE f.user_id = ${user_id}::uuid
37
+ `;
38
+
39
+ // โœ… Aggregate
40
+ const majorCount: Record<string, number> = {};
41
+
42
+ profiles.forEach((profile) => {
43
+ [profile.major_edu_1, profile.major_edu_2, profile.major_edu_3]
44
+ .filter(Boolean)
45
+ .forEach((major) => {
46
+ if (!major) return;
47
+ majorCount[major] = (majorCount[major] || 0) + 1;
48
+ });
49
+ });
50
+
51
+ // โœ… Format for chart
52
+ const result = Object.entries(majorCount).map(([name, value]) => ({
53
+ name,
54
+ value,
55
+ color: randomColor(),
56
+ }));
57
+
58
+ return NextResponse.json(result);
59
+ } catch (error) {
60
+ console.error("Chart major error:", error);
61
+ return NextResponse.json(
62
+ { message: "Failed to fetch chart data" },
63
+ { status: 500 }
64
+ );
65
+ }
66
  }
src/app/api/cv-profile/charts/university/route.ts CHANGED
@@ -2,35 +2,67 @@ import { prisma } from "@/lib/prisma";
2
  import { NextResponse } from "next/server";
3
 
4
  function randomColor() {
5
- return `#${Math.floor(Math.random() * 16777215).toString(16)}`;
 
 
6
  }
7
 
8
- export async function GET() {
9
- const profiles = await prisma.cv_profile.findMany({
10
- select: {
11
- univ_edu_1: true,
12
- univ_edu_2: true,
13
- univ_edu_3: true,
14
- },
15
- });
16
-
17
- const universityCount: Record<string, number> = {};
18
-
19
- profiles.forEach((profile) => {
20
- [profile.univ_edu_1, profile.univ_edu_2, profile.univ_edu_3]
21
- .filter(Boolean)
22
- .forEach((univ) => {
23
- if (!univ) return;
24
-
25
- universityCount[univ] = (universityCount[univ] || 0) + 1;
26
- });
27
- });
28
-
29
-
30
-
31
- return NextResponse.json(Object.entries(universityCount).map(([name, value]) => ({
32
- name,
33
- value,
34
- color: randomColor(),
35
- })));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  }
 
2
  import { NextResponse } from "next/server";
3
 
4
  function randomColor() {
5
+ return `#${Math.floor(Math.random() * 16777215)
6
+ .toString(16)
7
+ .padStart(6, "0")}`;
8
  }
9
 
10
+ export async function GET(req: Request) {
11
+ try {
12
+ const { searchParams } = new URL(req.url);
13
+ const user_id = searchParams.get("user_id");
14
+
15
+ if (!user_id) {
16
+ return NextResponse.json(
17
+ { message: "user_id is required" },
18
+ { status: 400 }
19
+ );
20
+ }
21
+
22
+ // โœ… RAW QUERY WITH JOIN
23
+ const profiles = await prisma.$queryRaw<
24
+ {
25
+ univ_edu_1: string | null;
26
+ univ_edu_2: string | null;
27
+ univ_edu_3: string | null;
28
+ }[]
29
+ >`
30
+ SELECT
31
+ p.univ_edu_1,
32
+ p.univ_edu_2,
33
+ p.univ_edu_3
34
+ FROM cv_profile p
35
+ INNER JOIN cv_file f ON f.file_id = p.file_id
36
+ WHERE f.user_id = ${user_id}::uuid
37
+ `;
38
+
39
+ // โœ… Aggregate
40
+ const universityCount: Record<string, number> = {};
41
+
42
+ profiles.forEach((profile) => {
43
+ [profile.univ_edu_1, profile.univ_edu_2, profile.univ_edu_3]
44
+ .filter(Boolean)
45
+ .forEach((univ) => {
46
+ if (!univ) return;
47
+ universityCount[univ] = (universityCount[univ] || 0) + 1;
48
+ });
49
+ });
50
+
51
+ // โœ… Format + Sort
52
+ const result = Object.entries(universityCount)
53
+ .map(([name, value]) => ({
54
+ name,
55
+ value,
56
+ color: randomColor(),
57
+ }))
58
+ .sort((a, b) => b.value - a.value);
59
+
60
+ return NextResponse.json(result);
61
+ } catch (error) {
62
+ console.error("Chart university error:", error);
63
+ return NextResponse.json(
64
+ { message: "Failed to fetch chart data" },
65
+ { status: 500 }
66
+ );
67
+ }
68
  }
src/components/dashboard/charts/major-chart.tsx CHANGED
@@ -1,24 +1,27 @@
1
  'use client';
2
 
 
3
  import { authFetch } from '@/lib/api';
4
  import { useQuery } from '@tanstack/react-query';
5
  import { ChartPie } from '../../ui/ChartPie';
6
 
7
  export function MajorChart() {
8
- const fetchMajor = async (): Promise<{
 
9
  name: string
10
  value: number
11
  color: string
12
  }[]> => {
13
- const res = await authFetch(`/api/cv-profile/charts/major`)
 
14
  if (!res.ok) throw new Error("Failed to fetch major")
15
  return res.json()
16
  }
17
 
18
 
19
  const { data = [], isLoading } = useQuery({
20
- queryKey: ["charts-major"],
21
- queryFn: () => fetchMajor(),
22
  staleTime: 0, // always fresh
23
  placeholderData: (prev) => prev, // keep previous data while loading (no flicker)
24
  refetchOnWindowFocus: false,
 
1
  'use client';
2
 
3
+ import { useAuth } from '@/composables/useAuth';
4
  import { authFetch } from '@/lib/api';
5
  import { useQuery } from '@tanstack/react-query';
6
  import { ChartPie } from '../../ui/ChartPie';
7
 
8
  export function MajorChart() {
9
+ const { user } = useAuth()
10
+ const fetchMajor = async (userId: string): Promise<{
11
  name: string
12
  value: number
13
  color: string
14
  }[]> => {
15
+ if (!userId) return []
16
+ const res = await authFetch(`/api/cv-profile/charts/major?user_id=${userId}`)
17
  if (!res.ok) throw new Error("Failed to fetch major")
18
  return res.json()
19
  }
20
 
21
 
22
  const { data = [], isLoading } = useQuery({
23
+ queryKey: ["charts-major", user?.user_id],
24
+ queryFn: () => fetchMajor(user?.user_id ?? ""),
25
  staleTime: 0, // always fresh
26
  placeholderData: (prev) => prev, // keep previous data while loading (no flicker)
27
  refetchOnWindowFocus: false,
src/components/dashboard/charts/university-bar.tsx CHANGED
@@ -1,25 +1,28 @@
1
  'use client'
2
 
 
3
  import { authFetch } from '@/lib/api'
4
  import { useQuery } from '@tanstack/react-query'
5
  import { ChartBar } from '../../ui/ChartBar'
6
 
7
  export function UniversityBar() {
8
- const fetchScore = async (): Promise<
 
9
  {
10
  name: string
11
  value: number
12
  color: string
13
  }[]
14
  > => {
15
- const res = await authFetch(`/api/cv-profile/charts/university`)
 
16
  if (!res.ok) throw new Error('Failed to fetch score')
17
  return res.json()
18
  }
19
 
20
  const { data = [], isLoading } = useQuery({
21
- queryKey: ['charts-university'],
22
- queryFn: fetchScore,
23
  staleTime: 0,
24
  placeholderData: (prev) => prev,
25
  refetchOnWindowFocus: false,
 
1
  'use client'
2
 
3
+ import { useAuth } from '@/composables/useAuth'
4
  import { authFetch } from '@/lib/api'
5
  import { useQuery } from '@tanstack/react-query'
6
  import { ChartBar } from '../../ui/ChartBar'
7
 
8
  export function UniversityBar() {
9
+ const { user } = useAuth()
10
+ const fetchScore = async (userId: string): Promise<
11
  {
12
  name: string
13
  value: number
14
  color: string
15
  }[]
16
  > => {
17
+ if (!userId) return []
18
+ const res = await authFetch(`/api/cv-profile/charts/university?user_id=${userId}`)
19
  if (!res.ok) throw new Error('Failed to fetch score')
20
  return res.json()
21
  }
22
 
23
  const { data = [], isLoading } = useQuery({
24
+ queryKey: ['charts-university', user?.user_id],
25
+ queryFn: () => fetchScore(user?.user_id ?? ""),
26
  staleTime: 0,
27
  placeholderData: (prev) => prev,
28
  refetchOnWindowFocus: false,
src/components/ui/ChartBar.tsx CHANGED
@@ -21,35 +21,54 @@ export function ChartBar({
21
  loading,
22
  data
23
  }: ChartBarProps) {
 
24
  const rows = 8
25
- if (loading) return (
26
- <div
27
- className="w-full"
28
- style={{ height: rows * 40 }}
29
- >
30
- <div className="flex flex-col gap-4 animate-pulse">
31
- {Array.from({ length: rows }).map((_, index) => (
32
- <div key={index} className="flex items-center gap-4">
33
- {/* Fake Y label */}
34
- <div className="h-4 bg-gray-200 rounded w-48" />
35
 
36
- {/* Fake Bar */}
37
- <div
38
- className="h-5 bg-gray-200 rounded"
39
- style={{
40
- width: `${40 + Math.random() * 50}%`,
41
- }}
42
- />
43
- </div>
44
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  </div>
46
- </div>
47
- )
 
 
 
 
 
 
 
 
 
 
 
 
48
 
 
 
 
49
  return (
50
  <ResponsiveContainer
51
  width="100%"
52
- height={data.length * 40} // 40px per row
53
  >
54
  <BarChart
55
  data={data}
 
21
  loading,
22
  data
23
  }: ChartBarProps) {
24
+
25
  const rows = 8
 
 
 
 
 
 
 
 
 
 
26
 
27
+ // -----------------------
28
+ // ๐Ÿ”น LOADING STATE
29
+ // -----------------------
30
+ if (loading) {
31
+ return (
32
+ <div
33
+ className="w-full"
34
+ style={{ height: rows * 40 }}
35
+ >
36
+ <div className="flex flex-col gap-4 animate-pulse">
37
+ {Array.from({ length: rows }).map((_, index) => (
38
+ <div key={index} className="flex items-center gap-4">
39
+ <div className="h-4 bg-gray-200 rounded w-48" />
40
+ <div
41
+ className="h-5 bg-gray-200 rounded"
42
+ style={{
43
+ width: `${40 + Math.random() * 50}%`,
44
+ }}
45
+ />
46
+ </div>
47
+ ))}
48
+ </div>
49
  </div>
50
+ )
51
+ }
52
+
53
+ // -----------------------
54
+ // ๐Ÿ”น EMPTY STATE
55
+ // -----------------------
56
+ if (!data || data.length === 0) {
57
+ return (
58
+ <div className="w-full h-60 flex flex-col items-center justify-center text-gray-400">
59
+ <div className="w-16 h-40 border-4 border-dashed border-gray-200 rounded mb-4" />
60
+ <p className="text-sm">No data available</p>
61
+ </div>
62
+ )
63
+ }
64
 
65
+ // -----------------------
66
+ // ๐Ÿ”น CHART
67
+ // -----------------------
68
  return (
69
  <ResponsiveContainer
70
  width="100%"
71
+ height={Math.max(250, data.length * 40)}
72
  >
73
  <BarChart
74
  data={data}
src/components/ui/ChartPie.tsx CHANGED
@@ -19,32 +19,51 @@ export function ChartPie({
19
  loading,
20
  data
21
  }: ChartPieProps) {
 
22
  const items = 5
23
- if (loading) return (
24
- <div className="w-full flex items-center justify-center gap-10 animate-pulse">
25
-
26
- {/* Fake Pie Circle */}
27
- <div className="relative">
28
- <div className="w-48 h-48 bg-gray-200 rounded-full" />
29
- <div className="absolute inset-6 bg-white rounded-full" />
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  </div>
 
 
31
 
32
- {/* Fake Legend */}
33
- <div className="flex flex-col gap-4">
34
- {Array.from({ length: items }).map((_, index) => (
35
- <div key={index} className="flex items-center gap-3">
36
- <div className="w-4 h-4 bg-gray-200 rounded-sm" />
37
- <div className="h-4 bg-gray-200 rounded w-32" />
38
- </div>
39
- ))}
40
  </div>
 
 
41
 
42
- </div>
43
- )
 
44
  return (
45
  <ResponsiveContainer
46
  width="100%"
47
- height={data.length * 40} // 40px per row
48
  >
49
  <PieChart>
50
  <Pie
@@ -55,9 +74,10 @@ export function ChartPie({
55
  <Cell key={`cell-${index}`} fill={entry.color} />
56
  ))}
57
  </Pie>
 
58
  <Tooltip formatter={(value) => `${value}%`} />
59
  <Legend />
60
  </PieChart>
61
  </ResponsiveContainer>
62
  );
63
- }
 
19
  loading,
20
  data
21
  }: ChartPieProps) {
22
+
23
  const items = 5
24
+
25
+ // -----------------------
26
+ // ๐Ÿ”น LOADING STATE
27
+ // -----------------------
28
+ if (loading) {
29
+ return (
30
+ <div className="w-full flex items-center justify-center gap-10 animate-pulse">
31
+ <div className="relative">
32
+ <div className="w-48 h-48 bg-gray-200 rounded-full" />
33
+ <div className="absolute inset-6 bg-white rounded-full" />
34
+ </div>
35
+
36
+ <div className="flex flex-col gap-4">
37
+ {Array.from({ length: items }).map((_, index) => (
38
+ <div key={index} className="flex items-center gap-3">
39
+ <div className="w-4 h-4 bg-gray-200 rounded-sm" />
40
+ <div className="h-4 bg-gray-200 rounded w-32" />
41
+ </div>
42
+ ))}
43
+ </div>
44
  </div>
45
+ )
46
+ }
47
 
48
+ // -----------------------
49
+ // ๐Ÿ”น EMPTY STATE
50
+ // -----------------------
51
+ if (!data || data.length === 0) {
52
+ return (
53
+ <div className="w-full h-60 flex flex-col items-center justify-center text-gray-400">
54
+ <div className="w-40 h-40 border-4 border-dashed border-gray-200 rounded-full mb-4" />
55
+ <p className="text-sm">No data available</p>
56
  </div>
57
+ )
58
+ }
59
 
60
+ // -----------------------
61
+ // ๐Ÿ”น CHART
62
+ // -----------------------
63
  return (
64
  <ResponsiveContainer
65
  width="100%"
66
+ height={Math.max(250, data.length * 40)}
67
  >
68
  <PieChart>
69
  <Pie
 
74
  <Cell key={`cell-${index}`} fill={entry.color} />
75
  ))}
76
  </Pie>
77
+
78
  <Tooltip formatter={(value) => `${value}%`} />
79
  <Legend />
80
  </PieChart>
81
  </ResponsiveContainer>
82
  );
83
+ }