diff --git a/backend/models/responses/stats.go b/backend/models/responses/stats.go index dd9f52f..5e9462c 100644 --- a/backend/models/responses/stats.go +++ b/backend/models/responses/stats.go @@ -1,16 +1,17 @@ package responses type Stats struct { - TotalJobs int `json:"totalJobs"` - Current StatTrack `json:"current"` - Historical StatTrack `json:"historical"` + Total int `json:"total"` + Current Stat `json:"current"` + Historical Stat `json:"historical"` } -type StatTrack struct { +type Stat struct { Applied int `json:"applied"` Interview int `json:"interview"` Offer int `json:"offer"` Rejected int `json:"rejected"` Other int `json:"other"` Accepted int `json:"accepted"` + Withdrawn int `json:"withdrawn"` } diff --git a/backend/services/job_service.go b/backend/services/job_service.go index b61be99..983ad53 100644 --- a/backend/services/job_service.go +++ b/backend/services/job_service.go @@ -114,10 +114,11 @@ func (s *JobService) CreateNewJob(ctx context.Context, userId string, job reques errChan <- err return } - mimeType = "png" + mimeType = "image/png" } - filename := fmt.Sprintf("%s/%s.%s", userId, jobEntity.Id, mimeType) + extension := strings.Split(mimeType, "/")[1] + filename := fmt.Sprintf("%s/%s.%s", userId, jobEntity.Id, extension) downloadUrl, err := s.Storage.UploadFile(ctx, filename, b) if err != nil { log.Println("Error uploading image: ", err) @@ -236,6 +237,8 @@ func (s *JobService) GetStats(ctx context.Context, userId string) (responses.Sta stats.Current.Rejected++ case "accepted": stats.Current.Accepted++ + case "withdrawn": + stats.Current.Withdrawn++ default: stats.Current.Other++ } @@ -256,13 +259,15 @@ func (s *JobService) GetStats(ctx context.Context, userId string) (responses.Sta stats.Historical.Rejected++ case "accepted": stats.Historical.Accepted++ + case "withdrawn": + stats.Historical.Withdrawn++ default: stats.Historical.Other++ } } } - stats.TotalJobs = len(jobs) + stats.Total = len(jobs) return stats, nil } diff --git a/extension/src/pages/popup/hooks/use-screenshot.hook.ts b/extension/src/pages/popup/hooks/use-screenshot.hook.ts index 0cb0699..ac7bb89 100644 --- a/extension/src/pages/popup/hooks/use-screenshot.hook.ts +++ b/extension/src/pages/popup/hooks/use-screenshot.hook.ts @@ -28,7 +28,10 @@ const useScreenshot = () => { await chrome.scripting.executeScript({ target: { tabId: activeTab.id }, - function: () => window.scrollTo(0, 0), + function: async () => { + window.scrollTo({ top: 0, left: 0, behavior: "instant" }); + await new Promise((resolve) => setTimeout(resolve, 100)); + }, }); async function captureScreenshot() { @@ -63,7 +66,7 @@ const useScreenshot = () => { } } - captureScreenshot(); + await captureScreenshot(); setCapturing(false); } diff --git a/extension/src/pages/popup/routes/jobs/add.lazy.tsx b/extension/src/pages/popup/routes/jobs/add.lazy.tsx index c18f1d7..815dbc5 100644 --- a/extension/src/pages/popup/routes/jobs/add.lazy.tsx +++ b/extension/src/pages/popup/routes/jobs/add.lazy.tsx @@ -8,13 +8,15 @@ import { import { createLazyFileRoute } from "@tanstack/react-router"; import useMessage from "@pages/popup/hooks/use-message.hook"; import useScreenshot from "@pages/popup/hooks/use-screenshot.hook"; -import { Aperture } from "lucide-react"; +import { Aperture, Loader2 } from "lucide-react"; +import { useState } from "react"; export const Route = createLazyFileRoute("/jobs/add")({ component: AddJob, }); function AddJob() { + const [capturing, setCapturing] = useState(false); const { captureFullPageScreenshot, canvasRef } = useScreenshot(); const { data: token } = useMessage({ type: "userToken" }); @@ -22,8 +24,10 @@ function AddJob() { useAddJob(token); const onCapture = () => { + setCapturing(true); captureFullPageScreenshot((dataUrl) => { setJobImage(dataUrl); + setCapturing(false); }); }; @@ -38,12 +42,13 @@ function AddJob() { Add - + {!capturing && } + {capturing && } Cancel diff --git a/extension/src/pages/popup/routes/jobs/index.tsx b/extension/src/pages/popup/routes/jobs/index.tsx index 5c5892b..e3b2640 100644 --- a/extension/src/pages/popup/routes/jobs/index.tsx +++ b/extension/src/pages/popup/routes/jobs/index.tsx @@ -79,6 +79,7 @@ const AllJobsTableAsync: React.FC = ({ token }) => { onUpdateJob={onUpdateJob} isPendingDelete={isPendingDelete} jobs={jobs} + addNewJobUrl="/jobs/add" /> ); diff --git a/frontend/src/components/AllJobsTable.tsx b/frontend/src/components/AllJobsTable.tsx index b780b4e..3f4fcc1 100644 --- a/frontend/src/components/AllJobsTable.tsx +++ b/frontend/src/components/AllJobsTable.tsx @@ -269,11 +269,13 @@ interface AllJobsTableProps { onViewJob: (job: JobResponse) => void; onUpdateJob: (job: JobResponse) => void; isPendingDelete: boolean; + addNewJobUrl: "/jobs/add/modal" | "/jobs/add"; } const AllJobsTable: React.FC = ({ jobs, isPendingDelete, + addNewJobUrl, onDeleteJob, onDeleteJobs, onViewJob, @@ -305,7 +307,7 @@ const AllJobsTable: React.FC = ({ - + New Job diff --git a/frontend/src/components/AllJobsTableAsync.tsx b/frontend/src/components/AllJobsTableAsync.tsx index 98ca5b1..0eab205 100644 --- a/frontend/src/components/AllJobsTableAsync.tsx +++ b/frontend/src/components/AllJobsTableAsync.tsx @@ -39,6 +39,7 @@ const AllJobsTableAsync = () => { onUpdateJob={onUpdateJob} isPendingDelete={isPendingDelete} jobs={jobs} + addNewJobUrl="/jobs/add/modal" /> ); }; diff --git a/frontend/src/constants/job-form.constants.ts b/frontend/src/constants/job-form.constants.ts index aab9384..0cb30b5 100644 --- a/frontend/src/constants/job-form.constants.ts +++ b/frontend/src/constants/job-form.constants.ts @@ -56,6 +56,10 @@ export default class JobFormConstants { id: "rejected", label: "Rejected", }, + { + id: "withdrawn", + label: "Withdrawn", + }, ]; public static readonly DisabledJobStatuses = "rejected"; diff --git a/frontend/src/hooks/use-query.hook.ts b/frontend/src/hooks/use-query.hook.ts index 76c775e..851ddc0 100644 --- a/frontend/src/hooks/use-query.hook.ts +++ b/frontend/src/hooks/use-query.hook.ts @@ -13,6 +13,7 @@ import { auth } from "@/constants/firebase"; import { useIdToken } from "react-firebase-hooks/auth"; import { env } from "@/env"; import AppConstants from "@/constants/app.constants"; +import StatsResponse from "@/models/responses/stats.response"; const getBaseUrl = () => { if (import.meta.env.MODE === "production") { @@ -290,9 +291,7 @@ export const useGetJobStatsQuery = () => { ); } - console.log(await response.json()); - - return await response.json(); + return new StatsResponse(await response.json()); }, }); }; diff --git a/frontend/src/models/responses/stats.response.ts b/frontend/src/models/responses/stats.response.ts new file mode 100644 index 0000000..d1b5bd1 --- /dev/null +++ b/frontend/src/models/responses/stats.response.ts @@ -0,0 +1,47 @@ +export type StatsResponseJson = { + total: number; + current: StatResponseJson; + historical: StatResponseJson; +}; + +export type StatResponseJson = { + applied: number; + interview: number; + offer: number; + rejected: number; + other: number; + accepted: number; + withdrawn: number; +}; + +export default class StatsResponse { + constructor(data: StatsResponseJson) { + this.total = data.total; + this.current = new StatResponse(data.current); + this.historical = new StatResponse(data.historical); + } + + public total: number; + public current: StatResponse; + public historical: StatResponse; +} + +export class StatResponse { + constructor(data: StatResponseJson) { + this.applied = data.applied; + this.interview = data.interview; + this.offer = data.offer; + this.rejected = data.rejected; + this.other = data.other; + this.accepted = data.accepted; + this.withdrawn = data.withdrawn; + } + + public applied: number; + public interview: number; + public offer: number; + public rejected: number; + public other: number; + public accepted: number; + public withdrawn: number; +} diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 1ec043d..dacae06 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -20,6 +20,7 @@ import { Route as IndexImport } from './routes/index' const JobsLazyImport = createFileRoute('/jobs')() const JobsAddLazyImport = createFileRoute('/jobs/add')() const JobsJobIdLazyImport = createFileRoute('/jobs/$jobId')() +const JobsStatsLazyImport = createFileRoute('/jobs/stats')() const JobsAddModalLazyImport = createFileRoute('/jobs/add/modal')() const JobsJobIdModalLazyImport = createFileRoute('/jobs/$jobId/modal')() @@ -45,6 +46,11 @@ const JobsJobIdLazyRoute = JobsJobIdLazyImport.update({ getParentRoute: () => rootRoute, } as any).lazy(() => import('./routes/jobs_.$jobId.lazy').then((d) => d.Route)) +const JobsStatsLazyRoute = JobsStatsLazyImport.update({ + path: '/stats', + getParentRoute: () => JobsLazyRoute, +} as any).lazy(() => import('./routes/jobs.stats.lazy').then((d) => d.Route)) + const JobsAddModalLazyRoute = JobsAddModalLazyImport.update({ path: '/add/modal', getParentRoute: () => JobsLazyRoute, @@ -71,6 +77,10 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof JobsLazyImport parentRoute: typeof rootRoute } + '/jobs/stats': { + preLoaderRoute: typeof JobsStatsLazyImport + parentRoute: typeof JobsLazyImport + } '/jobs/$jobId': { preLoaderRoute: typeof JobsJobIdLazyImport parentRoute: typeof rootRoute @@ -94,7 +104,11 @@ declare module '@tanstack/react-router' { export const routeTree = rootRoute.addChildren([ IndexRoute, - JobsLazyRoute.addChildren([JobsJobIdModalLazyRoute, JobsAddModalLazyRoute]), + JobsLazyRoute.addChildren([ + JobsStatsLazyRoute, + JobsJobIdModalLazyRoute, + JobsAddModalLazyRoute, + ]), JobsJobIdLazyRoute, JobsAddLazyRoute, ]) diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index 8d0e378..c9e9c5f 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -53,6 +53,11 @@ const Root = () => { Jobs + + + Stats + + diff --git a/frontend/src/routes/jobs.stats.lazy.tsx b/frontend/src/routes/jobs.stats.lazy.tsx new file mode 100644 index 0000000..5b1fba8 --- /dev/null +++ b/frontend/src/routes/jobs.stats.lazy.tsx @@ -0,0 +1,117 @@ +import { createLazyFileRoute, useRouter } from "@tanstack/react-router"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useGetJobStatsQuery } from "@/hooks/use-query.hook"; +import { Button, ScrollArea } from "@/components"; +import { StatResponse } from "@/models/responses/stats.response"; +import React from "react"; +import { Skeleton } from "@/components/ui/skeleton"; + +export const Route = createLazyFileRoute("/jobs/stats")({ + component: Stats, +}); + +function Stats() { + const { data, isPending } = useGetJobStatsQuery(); + + const router = useRouter(); + + const onClose = () => { + router.history.back(); + }; + + return ( + <> + + + + Stats + + Track your job application progress + + + + {data && ( + + + Total Jobs Applied + {data?.total} + + + Current Status + + The current status of all your job applications + + + + + Historical Status + + This includes all status changes including statuses that + have been updated + + + + + )} + {isPending && ( + + + + + + + + + + + )} + + + + Close + + + + + > + ); +} + +interface StatRowProps { + stat: StatResponse; +} + +const StatRow: React.FC = ({ stat }) => { + return ( + + + Applied {stat.applied} + + + Interview {stat.interview} + + + Offer {stat.offer} + + + Rejected {stat.rejected} + + + Other {stat.other} + + + Accepted {stat.accepted} + + + Withdrawn {stat.withdrawn} + + + ); +};
Total Jobs Applied
{data?.total}
Current Status
+ The current status of all your job applications +
Historical Status
+ This includes all status changes including statuses that + have been updated +
+ Applied {stat.applied} +
+ Interview {stat.interview} +
+ Offer {stat.offer} +
+ Rejected {stat.rejected} +
+ Other {stat.other} +
+ Accepted {stat.accepted} +
+ Withdrawn {stat.withdrawn} +