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)
|
|
|
|
|
|
|
| 6 |
}
|
| 7 |
|
| 8 |
-
export async function GET() {
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 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)
|
|
|
|
|
|
|
| 6 |
}
|
| 7 |
|
| 8 |
-
export async function GET() {
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 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
|
|
|
|
| 9 |
name: string
|
| 10 |
value: number
|
| 11 |
color: string
|
| 12 |
}[]> => {
|
| 13 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 9 |
{
|
| 10 |
name: string
|
| 11 |
value: number
|
| 12 |
color: string
|
| 13 |
}[]
|
| 14 |
> => {
|
| 15 |
-
|
|
|
|
| 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 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
</div>
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
|
|
|
|
|
|
|
|
|
| 49 |
return (
|
| 50 |
<ResponsiveContainer
|
| 51 |
width="100%"
|
| 52 |
-
height={data.length * 40}
|
| 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 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
</div>
|
|
|
|
|
|
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
</div>
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
|
|
|
| 44 |
return (
|
| 45 |
<ResponsiveContainer
|
| 46 |
width="100%"
|
| 47 |
-
height={data.length * 40}
|
| 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 |
+
}
|