Skip to content

Commit 0964200

Browse files
Merge pull request #4010 from OneCommunityGlobal/Neeraj_Job_Posting_Most_Popular_Graph_Frontend
Neeraj Created a horizontal bar graph comparing the competitiveness of each role
2 parents f33955c + f1c090d commit 0964200

7 files changed

Lines changed: 480 additions & 2 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React, { useState, useEffect, useMemo } from "react";
2+
import axios from "axios";
3+
import { useSelector } from "react-redux";
4+
import { ENDPOINTS } from "../../../utils/URL";
5+
import JobAnalyticsFilters from "./JobAnalyticsFilters";
6+
import JobAnalyticsGraph from "./JobAnalyticsGraph";
7+
import styles from "./JobAnalyticsPage.module.css";
8+
9+
const JobAnalyticsCompetitiveRolesPage = () => {
10+
const darkMode = useSelector((state) => state.theme.darkMode);
11+
12+
const [filters, setFilters] = useState({
13+
dateMode: "All",
14+
startDate: "",
15+
endDate: "",
16+
roles: "All",
17+
granularity: "totals",
18+
});
19+
20+
const [data, setData] = useState([]);
21+
const [loading, setLoading] = useState(false);
22+
23+
const requestUrl = useMemo(() => {
24+
const start = filters.dateMode === "Custom" ? filters.startDate : "";
25+
const end = filters.dateMode === "Custom" ? filters.endDate : "";
26+
const gran =
27+
filters.dateMode === "Custom" && filters.granularity !== "totals"
28+
? filters.granularity
29+
: undefined;
30+
31+
return ENDPOINTS.JOB_ANALYTICS_QUERY(start, end, filters.roles, gran);
32+
}, [filters]);
33+
34+
useEffect(() => {
35+
let alive = true;
36+
37+
(async () => {
38+
setLoading(true);
39+
try {
40+
const resp = await axios.get(requestUrl);
41+
if (alive) {
42+
setData(Array.isArray(resp.data) ? resp.data : []);
43+
}
44+
} catch (e) {
45+
// eslint-disable-next-line no-console
46+
console.error("Error fetching job analytics:", e);
47+
if (alive) setData([]);
48+
} finally {
49+
if (alive) setLoading(false);
50+
}
51+
})();
52+
53+
return () => {
54+
alive = false;
55+
};
56+
}, [requestUrl]);
57+
58+
return (
59+
<div
60+
className={`${styles.jobAnalyticsPage} ${
61+
darkMode ? styles.dark : styles.light
62+
}`}
63+
>
64+
<div className={styles.jobAnalyticsFilters}>
65+
<JobAnalyticsFilters filters={filters} setFilters={setFilters} />
66+
</div>
67+
68+
{loading ? (
69+
<p>Loading…</p>
70+
) : (
71+
<JobAnalyticsGraph data={data} darkMode={darkMode} />
72+
)}
73+
</div>
74+
);
75+
};
76+
77+
export default JobAnalyticsCompetitiveRolesPage;
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import React, { useEffect, useState } from "react";
2+
import PropTypes from "prop-types";
3+
import axios from "axios";
4+
import { ENDPOINTS } from "../../../utils/URL";
5+
import styles from "./JobAnalyticsPage.module.css";
6+
7+
const GRANULARITY_OPTS = [
8+
{ value: "totals", label: "Totals" },
9+
{ value: "weekly", label: "Weekly" },
10+
{ value: "monthly", label: "Monthly" },
11+
{ value: "annually", label: "Annually" },
12+
];
13+
14+
function FilterField({ label, children }) {
15+
return (
16+
<label className={styles.filterLabel}>
17+
<span>{label}</span>
18+
{children}
19+
</label>
20+
);
21+
}
22+
23+
FilterField.propTypes = {
24+
label: PropTypes.string.isRequired,
25+
children: PropTypes.node.isRequired,
26+
};
27+
28+
function JobAnalyticsFilters({ filters, setFilters }) {
29+
const [roleOptions, setRoleOptions] = useState(["All"]);
30+
const [loadingRoles, setLoadingRoles] = useState(false);
31+
32+
useEffect(() => {
33+
let alive = true;
34+
35+
async function loadRoles() {
36+
setLoadingRoles(true);
37+
try {
38+
const resp = await axios.get(ENDPOINTS.JOB_ANALYTICS_ROLES);
39+
const roles = Array.isArray(resp.data) ? resp.data : [];
40+
41+
const sorted = [
42+
"All",
43+
...Array.from(new Set(roles)).sort((a, b) =>
44+
a.localeCompare(b)
45+
),
46+
];
47+
48+
if (alive) {
49+
setRoleOptions(sorted);
50+
if (!sorted.includes(filters.roles)) {
51+
setFilters((prev) => ({ ...prev, roles: "All" }));
52+
}
53+
}
54+
} catch (err) {
55+
// eslint-disable-next-line no-console
56+
console.error("Failed to load roles:", err);
57+
if (alive) setRoleOptions(["All"]);
58+
} finally {
59+
if (alive) setLoadingRoles(false);
60+
}
61+
}
62+
63+
loadRoles();
64+
return () => {
65+
alive = false;
66+
};
67+
}, [filters.roles, setFilters]);
68+
69+
const onChange = (e) => {
70+
const { name, value } = e.target;
71+
72+
if (name === "dateMode") {
73+
if (value === "All") {
74+
setFilters((prev) => ({
75+
...prev,
76+
dateMode: "All",
77+
startDate: "",
78+
endDate: "",
79+
granularity: "totals",
80+
}));
81+
return;
82+
}
83+
setFilters((prev) => ({ ...prev, dateMode: "Custom" }));
84+
return;
85+
}
86+
87+
if (name === "granularity" && filters.dateMode === "All") {
88+
setFilters((prev) => ({ ...prev, granularity: "totals" }));
89+
return;
90+
}
91+
92+
setFilters((prev) => ({ ...prev, [name]: value }));
93+
};
94+
95+
const nonTotalsDisabled = filters.dateMode !== "Custom";
96+
97+
return (
98+
<div className={styles.jobAnalyticsFilters}>
99+
<FilterField label="Dates">
100+
<select
101+
name="dateMode"
102+
value={filters.dateMode}
103+
onChange={onChange}
104+
className={styles.filterInput}
105+
>
106+
<option value="All">All</option>
107+
<option value="Custom">Custom</option>
108+
</select>
109+
</FilterField>
110+
111+
{filters.dateMode === "Custom" && (
112+
<>
113+
<FilterField label="Start Date">
114+
<input
115+
type="date"
116+
name="startDate"
117+
value={filters.startDate}
118+
onChange={onChange}
119+
className={styles.filterInput}
120+
/>
121+
</FilterField>
122+
123+
<FilterField label="End Date">
124+
<input
125+
type="date"
126+
name="endDate"
127+
value={filters.endDate}
128+
onChange={onChange}
129+
className={styles.filterInput}
130+
/>
131+
</FilterField>
132+
</>
133+
)}
134+
135+
<FilterField label="Role">
136+
<select
137+
name="roles"
138+
value={filters.roles}
139+
onChange={onChange}
140+
disabled={loadingRoles}
141+
className={styles.filterInput}
142+
>
143+
{roleOptions.map((r) => (
144+
<option key={r} value={r}>
145+
{r}
146+
</option>
147+
))}
148+
</select>
149+
</FilterField>
150+
151+
<FilterField label="Granularity">
152+
<select
153+
name="granularity"
154+
value={filters.granularity}
155+
onChange={onChange}
156+
className={styles.filterInput}
157+
>
158+
{GRANULARITY_OPTS.map((opt) => (
159+
<option
160+
key={opt.value}
161+
value={opt.value}
162+
disabled={opt.value !== "totals" && nonTotalsDisabled}
163+
>
164+
{opt.label}
165+
</option>
166+
))}
167+
</select>
168+
</FilterField>
169+
</div>
170+
);
171+
}
172+
173+
JobAnalyticsFilters.propTypes = {
174+
filters: PropTypes.shape({
175+
dateMode: PropTypes.string.isRequired,
176+
startDate: PropTypes.string,
177+
endDate: PropTypes.string,
178+
roles: PropTypes.string,
179+
granularity: PropTypes.string,
180+
}).isRequired,
181+
setFilters: PropTypes.func.isRequired,
182+
};
183+
184+
export default JobAnalyticsFilters;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import React from "react";
2+
import PropTypes from "prop-types";
3+
import { Bar } from "react-chartjs-2";
4+
import {
5+
Chart as ChartJS,
6+
CategoryScale,
7+
LinearScale,
8+
BarElement,
9+
Title,
10+
Tooltip,
11+
Legend,
12+
} from "chart.js";
13+
import ChartDataLabels from "chartjs-plugin-datalabels";
14+
import styles from "./JobAnalyticsPage.module.css";
15+
16+
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
17+
18+
export default function JobAnalyticsGraph({ data, darkMode }) {
19+
if (!Array.isArray(data) || data.length === 0) {
20+
return <p>No data available</p>;
21+
}
22+
23+
const chartData = {
24+
labels: data.map((d) => d.role),
25+
datasets: [
26+
{
27+
label: "Applications",
28+
data: data.map((d) => d.applications ?? d.count ?? 0),
29+
backgroundColor: darkMode ? "#3A506B" : "rgba(54, 162, 235, 0.7)",
30+
},
31+
],
32+
};
33+
34+
const chartOptions = {
35+
indexAxis: "y",
36+
responsive: true,
37+
maintainAspectRatio: false,
38+
plugins: {
39+
legend: { display: false },
40+
title: {
41+
display: true,
42+
text: "Most Competitive Roles",
43+
color: darkMode ? "#E0E0E0" : "#111111",
44+
font: { size: 18, weight: "bold" },
45+
},
46+
datalabels: {
47+
color: darkMode ? "#E0E0E0" : "#111111",
48+
anchor: "end",
49+
align: "left",
50+
offset: -5,
51+
formatter: (value) => value.toLocaleString(),
52+
font: { weight: "bold" },
53+
},
54+
},
55+
scales: {
56+
x: {
57+
title: {
58+
display: true,
59+
text: "Number of Applications",
60+
color: darkMode ? "#E0E0E0" : "#111111",
61+
font: { weight: "bold", size: 14 },
62+
},
63+
ticks: {
64+
color: darkMode ? "#E0E0E0" : "#111111",
65+
},
66+
grid: { color: darkMode ? "#333" : "#ddd" },
67+
},
68+
y: {
69+
title: {
70+
display: true,
71+
text: "Role",
72+
color: darkMode ? "#E0E0E0" : "#111111",
73+
font: { weight: "bold", size: 14 },
74+
},
75+
ticks: {
76+
color: darkMode ? "#E0E0E0" : "#111111",
77+
},
78+
grid: { color: darkMode ? "#333" : "#ddd" },
79+
},
80+
},
81+
};
82+
83+
return (
84+
<div className={styles.graphContainer}>
85+
<Bar data={chartData} options={chartOptions} plugins={[ChartDataLabels]} />
86+
</div>
87+
);
88+
}
89+
90+
JobAnalyticsGraph.propTypes = {
91+
data: PropTypes.arrayOf(
92+
PropTypes.shape({
93+
role: PropTypes.string.isRequired,
94+
applications: PropTypes.number,
95+
count: PropTypes.number,
96+
})
97+
),
98+
darkMode: PropTypes.bool,
99+
};
100+
101+
JobAnalyticsGraph.defaultProps = {
102+
data: [],
103+
darkMode: false,
104+
};

0 commit comments

Comments
 (0)