Skip to content

Commit a77174f

Browse files
authored
Merge branch 'main' into dependabot/npm_and_yarn/frontend/js-yaml-4.1.1
2 parents 737c865 + 6efc670 commit a77174f

14 files changed

Lines changed: 563 additions & 708 deletions

backend/requirements.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Deprecated==1.2.18
1717
dnspython==2.7.0
1818
email_validator==2.2.0
1919
en-core-web-sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.7.1/en_core_web_sm-3.7.1-py3-none-any.whl#sha256=86cc141f63942d4b2c5fcee06630fd6f904788d2f0ab005cce45aadb8fb73889
20-
fastapi==0.115.6
20+
fastapi>=0.127.0
2121
google-ai-generativelanguage==0.6.15
2222
google-api-core==2.24.2
2323
google-api-python-client==2.156.0
@@ -55,7 +55,7 @@ plotly==6.0.1
5555
pluggy==1.5.0
5656
preshed==3.0.9
5757
proto-plus==1.25.0
58-
protobuf==5.29.2
58+
protobuf==5.29.5
5959
psycopg2==2.9.10
6060
pyasn1==0.6.1
6161
pyasn1_modules==0.4.1
@@ -89,7 +89,7 @@ spacy-lookups-data==1.0.5
8989
SQLAlchemy==2.0.38
9090
sqlmodel==0.0.23
9191
srsly==2.5.0
92-
starlette==0.41.3
92+
starlette>=0.49.1,<0.51
9393
testcontainers==4.10.0
9494
thinc==8.2.5
9595
tqdm==4.66.6
@@ -99,7 +99,7 @@ types-tqdm==4.67.0.20250417
9999
typing_extensions==4.12.2
100100
tzdata==2024.2
101101
uritemplate==4.1.1
102-
urllib3==2.3.0
102+
urllib3==2.6.0
103103
uvicorn==0.34.2
104104
wasabi==1.1.3
105105
weasel==0.4.1

coding_interview_feedback.gs

Lines changed: 347 additions & 0 deletions
Large diffs are not rendered by default.

frontend/app/dashboard/page.tsx

Lines changed: 0 additions & 233 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,11 @@ import { useState, useEffect, useMemo } from "react";
33
import { useRouter } from "next/navigation";
44
import { addToast } from "@heroui/toast";
55
import React from "react";
6-
import { Sankey, ResponsiveContainer, Tooltip } from "recharts";
76
import posthog from "posthog-js";
87

98
import JobApplicationsDashboard, { Application } from "@/components/JobApplicationsDashboard";
10-
import ResponseRateCard from "@/components/response_rate_card";
11-
import UniqueOpenRateChart from "@/components/response_rate_chart";
129
import { checkAuth } from "@/utils/auth";
1310

14-
interface SankeyData {
15-
nodes: { name: string }[];
16-
links: { source: number; target: number; value: number }[];
17-
}
18-
1911
export default function Dashboard() {
2012
const router = useRouter();
2113
const [data, setData] = useState<Application[]>([]);
@@ -24,7 +16,6 @@ export default function Dashboard() {
2416
const [error, setError] = useState<string | null>(null);
2517
const [currentPage, setCurrentPage] = useState(1);
2618
const [totalPages, setTotalPages] = useState(1);
27-
const [sankeyData, setSankeyData] = useState<SankeyData | null>(null);
2819
const [searchTerm, setSearchTerm] = useState("");
2920
const [statusFilter, setStatusFilter] = useState("");
3021
const [companyFilter, setCompanyFilter] = useState("");
@@ -101,25 +92,6 @@ export default function Dashboard() {
10192
fetchData();
10293
}, [apiUrl, router, currentPage]);
10394

104-
useEffect(() => {
105-
// Fetch Sankey data
106-
const fetchSankey = async () => {
107-
try {
108-
const response = await fetch(`${apiUrl}/get-sankey-data`, {
109-
method: "GET",
110-
credentials: "include"
111-
});
112-
if (response.ok) {
113-
const result = await response.json();
114-
setSankeyData(result);
115-
}
116-
} catch {
117-
// ignore for now
118-
}
119-
};
120-
fetchSankey();
121-
}, [apiUrl, router, currentPage]);
122-
12395
// Filter data based on search term, status, company, and hide options
12496
const filteredData = useMemo(() => {
12597
return data.filter((item) => {
@@ -231,53 +203,6 @@ export default function Dashboard() {
231203
);
232204
}
233205

234-
async function downloadSankey() {
235-
setDownloading(true);
236-
try {
237-
const response = await fetch(`${apiUrl}/process-sankey`, {
238-
method: "GET",
239-
credentials: "include"
240-
});
241-
242-
if (!response.ok) {
243-
let description = "Something went wrong. Please try again.";
244-
245-
if (response.status === 429) {
246-
description = "Download limit reached. Please wait before trying again.";
247-
} else {
248-
description = "Please try again or contact help@justajobapp.com if the issue persists.";
249-
}
250-
251-
addToast({
252-
title: "Failed to download Sankey Diagram",
253-
description,
254-
color: "danger"
255-
});
256-
257-
return;
258-
}
259-
260-
// Create a download link to trigger the file download
261-
const blob = await response.blob();
262-
const link = document.createElement("a");
263-
const url = URL.createObjectURL(blob);
264-
link.href = url;
265-
link.download = `sankey_diagram_${new Date().toISOString().split("T")[0]}.png`;
266-
document.body.appendChild(link);
267-
link.click();
268-
document.body.removeChild(link);
269-
URL.revokeObjectURL(url);
270-
} catch {
271-
addToast({
272-
title: "Something went wrong",
273-
description: "Please try again",
274-
color: "danger"
275-
});
276-
} finally {
277-
setDownloading(false);
278-
}
279-
}
280-
281206
const handleRemoveItem = async (id: string) => {
282207
try {
283208
// Make a DELETE request to the backend
@@ -306,161 +231,6 @@ export default function Dashboard() {
306231
}
307232
};
308233

309-
const responseRateContent = (
310-
<>
311-
<div className="flex flex-col gap-4 mt-4 mb-6 md:flex-row">
312-
<div className="w-full md:w-[30%]">
313-
<ResponseRateCard />
314-
</div>
315-
<div className="md:w-[70%]">
316-
<UniqueOpenRateChart />
317-
</div>
318-
</div>
319-
</>
320-
);
321-
322-
const sankeyChartContent =
323-
sankeyData && sankeyData.nodes && sankeyData.nodes.length > 0 ? (
324-
<div className="bg-gray-100 dark:bg-gray-800 p-6 shadow-md rounded-lg">
325-
<div className="flex items-center justify-between mb-4">
326-
<h2 className="text-gray-700 dark:text-gray-300 text-base md:text-xl font-semibold">
327-
My Job Search
328-
</h2>
329-
<button
330-
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
331-
disabled={downloading}
332-
onClick={downloadSankey}
333-
>
334-
{downloading ? "Downloading..." : "Download PNG"}
335-
</button>
336-
</div>
337-
<div className="h-[450px] w-full relative">
338-
<ResponsiveContainer height="100%" width="100%">
339-
<Sankey
340-
data={sankeyData}
341-
link={{ stroke: "#cbd5e1", strokeOpacity: 0.4 }}
342-
margin={{ top: 10, bottom: 30, left: 10, right: 10 }} // Smaller margins
343-
node={(props) => {
344-
const isSource = props.x < 50;
345-
const nodeName = props.payload.name;
346-
const status = nodeName.split(" (")[0];
347-
348-
const getStatusColor = (status: string) => {
349-
const normalized = status.toLowerCase();
350-
switch (normalized) {
351-
case "applications":
352-
return "#3b82f6"; // Blue for source
353-
case "offer made":
354-
return "#10b981";
355-
case "rejection":
356-
return "#ef4444";
357-
case "interview invitation":
358-
return "#06b6d4";
359-
case "assessment sent":
360-
return "#8b5cf6";
361-
case "availability request":
362-
return "#f59e0b";
363-
case "application confirmation":
364-
return "#3b82f6";
365-
case "information request":
366-
return "#6b7280";
367-
case "inbound request":
368-
return "#10b981";
369-
case "action required":
370-
return "#84cc16";
371-
case "hiring freeze":
372-
return "#8b5cf6";
373-
case "withdrew application":
374-
return "#ec4899";
375-
default:
376-
return "#8884d8";
377-
}
378-
};
379-
380-
return (
381-
<g>
382-
<rect
383-
fill={getStatusColor(status)}
384-
fillOpacity={0.8}
385-
height={props.height}
386-
stroke={getStatusColor(status)}
387-
width={props.width}
388-
x={props.x}
389-
y={props.y}
390-
/>
391-
<text
392-
dominantBaseline="middle"
393-
fill="#333"
394-
fontSize={12}
395-
textAnchor={isSource ? "start" : "end"}
396-
x={isSource ? props.x + props.width + 8 : props.x - 8}
397-
y={props.y + props.height / 2}
398-
>
399-
{props.payload.name}
400-
</text>
401-
</g>
402-
);
403-
}}
404-
nodePadding={24}
405-
>
406-
<Tooltip
407-
content={({ active, payload }) => {
408-
if (active && payload && payload.length) {
409-
const { sourceNode, targetNode, value } = payload[0].payload;
410-
return (
411-
<div className="bg-white p-2 rounded shadow">
412-
<div>
413-
<strong>
414-
{sourceNode?.name}{targetNode?.name}
415-
</strong>
416-
</div>
417-
<div>Count: {value}</div>
418-
</div>
419-
);
420-
}
421-
return null;
422-
}}
423-
/>
424-
</Sankey>
425-
</ResponsiveContainer>
426-
<div className="absolute bottom-2 left-2 text-xs text-gray-500 opacity-70">
427-
{(() => {
428-
// Calculate date range from the data
429-
if (data && data.length > 0) {
430-
const dates = data
431-
.map((item) => (item.received_at ? new Date(item.received_at) : null))
432-
.filter((date) => date !== null);
433-
434-
if (dates.length > 0) {
435-
const minDate = new Date(Math.min(...dates.map((d) => d.getTime())));
436-
const maxDate = new Date(Math.max(...dates.map((d) => d.getTime())));
437-
438-
const formatDate = (date: Date) => {
439-
const month = date.toLocaleDateString("en-US", { month: "long" });
440-
const year = date.getFullYear();
441-
return { month, year };
442-
};
443-
444-
const minFormatted = formatDate(minDate);
445-
const maxFormatted = formatDate(maxDate);
446-
447-
let dateRange;
448-
if (minFormatted.year === maxFormatted.year) {
449-
dateRange = `${minFormatted.month} - ${maxFormatted.month} ${maxFormatted.year}`;
450-
} else {
451-
dateRange = `${minFormatted.month} ${minFormatted.year} - ${maxFormatted.month} ${maxFormatted.year}`;
452-
}
453-
454-
return `JustAJobApp.com • ${dateRange}`;
455-
}
456-
}
457-
return "JustAJobApp.com";
458-
})()}
459-
</div>
460-
</div>
461-
</div>
462-
) : null;
463-
464234
return (
465235
<JobApplicationsDashboard
466236
companyFilter={companyFilter}
@@ -471,14 +241,11 @@ export default function Dashboard() {
471241
hideRejections={hideRejections}
472242
loading={loading}
473243
normalizedJobTitleFilter={normalizedJobTitleFilter}
474-
responseRate={responseRateContent}
475-
sankeyChart={sankeyChartContent}
476244
searchTerm={searchTerm}
477245
statusFilter={statusFilter}
478246
totalPages={totalPages}
479247
onCompanyFilterChange={setCompanyFilter}
480248
onDownloadCsv={downloadCsv}
481-
onDownloadSankey={downloadSankey}
482249
onHideApplicationConfirmationsChange={setHideApplicationConfirmations}
483250
onHideRejectionsChange={setHideRejections}
484251
onNextPage={nextPage}

frontend/components/Footer.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ const Footer = () => {
102102
<div>
103103
<h3 className="text-lg font-semibold mb-4 text-emerald-700">JustAJobApp</h3>
104104
<p className="text-default-500 mb-4">Automate the "Second Job" of Job Searching.</p>
105+
<a
106+
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-700 transition-colors duration-200"
107+
href="/contributors"
108+
>
109+
<span className="mr-2">👋</span>
110+
Join the Community
111+
</a>
105112
</div>
106113

107114
<div>

frontend/components/JobApplicationsDashboard.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,8 @@ interface JobApplicationsDashboardProps {
4141
loading: boolean;
4242
downloading: boolean;
4343
onDownloadCsv: () => void;
44-
onDownloadSankey: () => void;
4544
onRemoveItem: (id: string) => void;
4645
initialSortKey?: string;
47-
responseRate?: React.ReactNode;
48-
sankeyChart?: React.ReactNode;
4946
searchTerm?: string;
5047
onSearchChange?: (term: string) => void;
5148
statusFilter?: string;
@@ -109,11 +106,8 @@ export default function JobApplicationsDashboard({
109106
loading,
110107
downloading,
111108
onDownloadCsv,
112-
onDownloadSankey,
113109
onRemoveItem,
114110
initialSortKey = "Date (Newest)",
115-
responseRate,
116-
sankeyChart,
117111
searchTerm = "",
118112
onSearchChange,
119113
statusFilter = "",
@@ -390,13 +384,11 @@ export default function JobApplicationsDashboard({
390384
</ModalContent>
391385
</Modal>
392386
<h1 className="text-2xl font-bold mt-0">{title}</h1>
393-
{responseRate}
394-
{sankeyChart && <div className="mb-6">{sankeyChart}</div>}
395387
<div className="flex flex-wrap items-center justify-between gap-4 mb-4">
396388
{/* Search and Filter Controls */}
397389
<div className="flex flex-wrap items-center gap-4 flex-1">
398390
{/* Search Input */}
399-
<div className="flex-1 max-w-md">
391+
<div className="max-w-md">
400392
<input
401393
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
402394
placeholder="Company"

0 commit comments

Comments
 (0)