From afa22704f8e18d951c6005e21e0037a7c3fc4ff0 Mon Sep 17 00:00:00 2001 From: Richard Pillaca Date: Thu, 11 Jun 2026 13:36:18 -0500 Subject: [PATCH 1/4] fix(review): address CodeRabbit findings on PR review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/SETUP.md: correct RESUME_PARSE_MODE default (queue, not queue-and-inline) and document JOB_RUNNER=inline behavior without workers - sign-in.test.ts: regression test proving lookalike/subdomain emails are rejected by the AUTH_ALLOWED_EMAIL_DOMAIN gate (endsWith(@ + domain) anchors on the @ separator, so no bypass exists) - dispatch.test.ts: test documenting that inline admin fan-out is fire-and-forget — send failures land in EmailLog, not the returned result --- docs/SETUP.md | 7 ++++--- src/lib/auth/sign-in.test.ts | 22 ++++++++++++++++++++++ src/server/jobs/dispatch.test.ts | 19 +++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/docs/SETUP.md b/docs/SETUP.md index e8c9efe..08e56be 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -254,9 +254,10 @@ After deploying or rotating keys, **restart both workers** so the new envs take ### 4.3 What happens without the workers -- With `RESUME_PARSE_MODE=queue-and-inline` (default), parsing happens inline during submission — the resume worker is optional for redundancy only. -- With `RESUME_PARSE_MODE=queue`, `enqueueResumeParseJob` returns immediately; the row stays in `PENDING` until the resume worker picks it up. -- `enqueueEmailJob` returns immediately; admin notification and applicant confirmation emails are never sent without the email worker — the applicant submission still succeeds. +- With `JOB_RUNNER=inline` (default on Vercel), no workers are needed: resume parsing and email delivery run after the response in the same serverless invocation. +- With `RESUME_PARSE_MODE=queue` (default), parsing is deferred to the job runner: in `worker` mode the row stays in `PENDING` until the resume worker picks it up; in `inline` mode it completes post-response. +- `RESUME_PARSE_MODE=queue-and-inline` additionally parses during submission — redundant unless you want a synchronous result alongside the queue. +- In `worker` mode, `enqueueEmailJob` returns immediately; admin notification and applicant confirmation emails are never sent without the email worker — the applicant submission still succeeds. Both are visible in `/admin/notifications` (parsing failures + email skips). diff --git a/src/lib/auth/sign-in.test.ts b/src/lib/auth/sign-in.test.ts index cb50700..e20b487 100644 --- a/src/lib/auth/sign-in.test.ts +++ b/src/lib/auth/sign-in.test.ts @@ -158,4 +158,26 @@ describe("handleSignIn — Google provider", () => { expect(allowed).toBe(true); expect(userUpsert).toHaveBeenCalledTimes(1); }); + + it("blocks lookalike domains that merely end with the allowed domain", async () => { + process.env.AUTH_ALLOWED_EMAIL_DOMAIN = "scoutlane.com"; + + const rejected = await handleSignIn({ + user: { email: "attacker@fakescoutlane.com" }, + account: { provider: "google" }, + }); + const subdomainRejected = await handleSignIn({ + user: { email: "attacker@evil.scoutlane.com" }, + account: { provider: "google" }, + }); + const allowed = await handleSignIn({ + user: { email: "jane@scoutlane.com" }, + account: { provider: "google" }, + }); + + expect(rejected).toBe(false); + expect(subdomainRejected).toBe(false); + expect(allowed).toBe(true); + expect(userUpsert).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/server/jobs/dispatch.test.ts b/src/server/jobs/dispatch.test.ts index 7378bf3..fba6ff4 100644 --- a/src/server/jobs/dispatch.test.ts +++ b/src/server/jobs/dispatch.test.ts @@ -166,4 +166,23 @@ describe("dispatchAdminNotificationEmails", () => { ); expect(result).toEqual({ enqueued: ["a@x.com", "b@x.com"], failed: [] }); }); + + it("keeps the fan-out result empty on inline send failures (they land in EmailLog)", async () => { + process.env.JOB_RUNNER = "inline"; + dispatchEmailJobMock.mockResolvedValue({ ok: false, skipped: false, error: "Send failed" }); + const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}); + + try { + const result = await dispatchAdminNotificationEmails(input); + await flushInlineTasks(); + + // Inline delivery is fire-and-forget post-response: failures are logged + // to EmailLog by the send functions, never surfaced to the caller. + expect(dispatchEmailJobMock).toHaveBeenCalledTimes(2); + expect(result).toEqual({ enqueued: ["a@x.com", "b@x.com"], failed: [] }); + expect(consoleError).toHaveBeenCalledTimes(2); + } finally { + consoleError.mockRestore(); + } + }); }); From 4914ee49543b713f3904d1d2cc549ecf45c47d84 Mon Sep 17 00:00:00 2001 From: Richard Pillaca Date: Thu, 11 Jun 2026 13:36:44 -0500 Subject: [PATCH 2/4] docs: note CodeRabbit review triage in session report --- ...2026-06-11-resume-pipeline-oauth-emails.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/session-reports/2026-06-11-resume-pipeline-oauth-emails.md diff --git a/docs/session-reports/2026-06-11-resume-pipeline-oauth-emails.md b/docs/session-reports/2026-06-11-resume-pipeline-oauth-emails.md new file mode 100644 index 0000000..cf1b537 --- /dev/null +++ b/docs/session-reports/2026-06-11-resume-pipeline-oauth-emails.md @@ -0,0 +1,38 @@ +# 2026-06-11 — Resume pipeline, OAuth, emails, MCP cleanup + +## Goal + +"fix mcp, connect to composio … fix scoutlane in the parsing whenever i upload a pdf it does not parse correctly … preferable gemini 2.5 flash … word is not embed, pdf is embeded, word cna be download, but pdf not found. The embeed needs fixing, then the login with my google email … fix the bugs with notifications and setting up the emails directly for people onces they submit their application." + +## What landed (PR #89, branch `fix/resume-pipeline-and-notifications`) + +- `5ae6980` fix(resume): pdf-parse via dynamic import compatible with Next bundling (+ committed PDF fixture, un-skipped test) +- `133fa2d` chore: .gitattributes marking binary assets (prevents fixture corruption) +- `8d8d3a4` feat(llm): default model `google/gemini-2.5-flash` + fallbacks, tightened prompt +- (storage) feat(resumes): shared disk→DB resolution, Content-Disposition, clearer 404 +- (preview) feat(resumes): DOCX→sanitized-HTML inline preview, authenticated route, sandboxed iframe +- (jobs) feat(jobs): `JOB_RUNNER=worker|inline` dispatcher; inline `after()` path = no workers on Vercel +- `ac86c72` fix(auth): conflicting Google env warning, Google users upserted with org, `AUTH_ALLOWED_EMAIL_DOMAIN` +- `b9ddcef` fix(jobs): sequential inline admin fan-out (Resend rate limit) +- MCP: removed broken `vercel` (SSE) + misconfigured `caveman-shrink`; Composio already connected +- Local `.env`: Google creds migrated legacy→`AUTH_GOOGLE_*` (root cause of broken login: empty `AUTH_GOOGLE_*` shadowing real legacy values), `JOB_RUNNER="inline"`, model updated + +E2E smoke verified (prod build, no workers): PDF parse COMPLETED via gemini-2.5-flash; DOCX parse COMPLETED; preview route 401 logged-out; EmailLog rows written for confirmation + admin fan-out. + +CodeRabbit review triage (`afa2270`): fixed SETUP.md §4.3 (RESUME_PARSE_MODE default is `queue`; documented JOB_RUNNER=inline), added domain-allowlist bypass regression test and inline fan-out failure-behavior test. Skipped as invalid: preview-route contentType null guard (type is always `string`), endsWith "subdomain bypass" (the `@` anchor already enforces exact domain), synchronous inline sends (fire-and-forget by design; failures land in EmailLog). + +## Still open + +- Merge PR #89 → main, then main → master to deploy +- Vercel env: set `AUTH_GOOGLE_ID/SECRET`, `AUTH_SECRET`, `OPENROUTER_API_KEY`, `RESEND_API_KEY`; verify GCP redirect URI for prod domain +- Verify a Resend domain for production `EMAIL_FROM` (test sender only delivers to account owner) +- Follow-up: `/api/resumes/*` unauthenticated (pre-existing) + +## Next recommended action + +Merge PR #89, set the Vercel env vars above, then smoke a PDF application on production. + +## Risks/blockers encountered + +- Resend free tier: unverified domain blocks delivery to non-owner addresses (logged in EmailLog, expected) +- Inline LLM call bounded by serverless maxDuration (60s); Retry button is the recovery path From b2cea013e715dadc3d8d34822557de88f9f2ffc2 Mon Sep 17 00:00:00 2001 From: Richard Pillaca Date: Thu, 11 Jun 2026 14:37:53 -0500 Subject: [PATCH 3/4] fix(auth): never enable dev login on production deployments The dev credentials provider (any email -> ADMIN) was registered whenever Google OAuth credentials were missing, which left admin sign-in wide open on deployments without AUTH_GOOGLE_* set. It is now restricted to NODE_ENV=development, with an explicit ALLOW_DEV_LOGIN=1 escape hatch for local production-build smoke tests. Also on the signin page: - hide the Google button when OAuth is not configured (clicking it previously bounced through /api/auth/signin back to /signin with no feedback - the reported refresh loop) - surface NextAuth ?error= codes as a visible alert - show a clear notice when no sign-in method is available And accept .txt resumes end-to-end (upload guard, schema message, file input accept list); text extraction and inline preview already support plain text. Co-Authored-By: Claude --- .env.example | 5 ++++ src/app/signin/_components/SignInForm.tsx | 36 ++++++++++++++++++++++- src/app/signin/page.tsx | 5 ++-- src/components/public/ApplicationForm.tsx | 4 +-- src/lib/auth/auth.config.test.ts | 14 ++++++++- src/lib/auth/auth.config.ts | 11 ++++++- src/lib/storage/upload-limits.ts | 3 +- src/schemas/application.ts | 2 +- 8 files changed, 71 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index e2e5280..bc38aa0 100644 --- a/.env.example +++ b/.env.example @@ -52,6 +52,11 @@ RESUME_PARSE_MODE="queue" # after(); no worker needed. Default on Vercel. JOB_RUNNER="" +# Dev-login escape hatch for local production-build smoke tests ONLY. +# The dev provider grants ADMIN to any email -- NEVER set this on a deployed +# environment. Dev login is otherwise enabled only under `next dev`. +ALLOW_DEV_LOGIN="" + # ── File Storage (Google Cloud Storage) ────────────────────────────────────── # Create a service account at https://console.cloud.google.com/iam-admin/serviceaccounts # and grant the Storage Object Admin role on the bucket. diff --git a/src/app/signin/_components/SignInForm.tsx b/src/app/signin/_components/SignInForm.tsx index a6584b5..207c828 100644 --- a/src/app/signin/_components/SignInForm.tsx +++ b/src/app/signin/_components/SignInForm.tsx @@ -8,11 +8,27 @@ import { AnimatedBackground } from "@/components/AnimatedBackground"; interface SignInFormProps { showDevLogin: boolean; + googleEnabled: boolean; } -export function SignInForm({ showDevLogin }: SignInFormProps) { +const AUTH_ERROR_MESSAGES: Record = { + AccessDenied: + "Sign-in was denied. Your account is not allowed to access this workspace.", + OAuthCallbackError: + "Google sign-in could not be completed. Please try again.", + OAuthSignInError: "Could not start Google sign-in. Please try again.", + Configuration: + "Sign-in is misconfigured on the server. Contact an administrator.", + Verification: "The sign-in link is no longer valid. Please try again.", +}; + +export function SignInForm({ showDevLogin, googleEnabled }: SignInFormProps) { const searchParams = useSearchParams(); const callbackUrl = searchParams.get("callbackUrl") || "/admin"; + const errorCode = searchParams.get("error"); + const errorMessage = errorCode + ? AUTH_ERROR_MESSAGES[errorCode] ?? "Sign-in failed. Please try again." + : null; const [email, setEmail] = useState("admin@scoutlane.local"); const [loading, setLoading] = useState(false); @@ -48,6 +64,23 @@ export function SignInForm({ showDevLogin }: SignInFormProps) {
+ {errorMessage && ( +
+ {errorMessage} +
+ )} + + {!googleEnabled && !showDevLogin && ( +

+ Sign-in is not configured on this deployment. An administrator + must set the Google OAuth environment variables. +

+ )} + + {googleEnabled && ( + )} {showDevLogin && ( <> diff --git a/src/app/signin/page.tsx b/src/app/signin/page.tsx index 79d7240..425d067 100644 --- a/src/app/signin/page.tsx +++ b/src/app/signin/page.tsx @@ -1,13 +1,14 @@ import { Suspense } from "react"; -import { isDevLoginAllowed } from "@/lib/auth/auth.config"; +import { isDevLoginAllowed, isGoogleAuthConfigured } from "@/lib/auth/auth.config"; import { SignInForm } from "./_components/SignInForm"; export default function SignInPage() { const showDevLogin = isDevLoginAllowed(); + const googleEnabled = isGoogleAuthConfigured(); return ( - + ); } diff --git a/src/components/public/ApplicationForm.tsx b/src/components/public/ApplicationForm.tsx index 93929c1..d061df9 100644 --- a/src/components/public/ApplicationForm.tsx +++ b/src/components/public/ApplicationForm.tsx @@ -241,7 +241,7 @@ export function ApplicationForm({ jobSlug, customFields = [] }: ApplicationFormP { onChange(event.target.files?.[0]); @@ -249,7 +249,7 @@ export function ApplicationForm({ jobSlug, customFields = [] }: ApplicationFormP />
- PDF, DOC, DOCX, or CSV up to 5 MB + PDF, DOC, DOCX, TXT, or CSV up to 5 MB
)} diff --git a/src/lib/auth/auth.config.test.ts b/src/lib/auth/auth.config.test.ts index 02c106e..39f210a 100644 --- a/src/lib/auth/auth.config.test.ts +++ b/src/lib/auth/auth.config.test.ts @@ -105,10 +105,22 @@ describe("isDevLoginAllowed", () => { expect(isDevLoginAllowed()).toBe(false); }); - it("allows dev login in production when Google auth is NOT configured", () => { + it("disallows dev login in production even when Google auth is NOT configured", () => { vi.stubEnv("NODE_ENV", "production"); + expect(isDevLoginAllowed()).toBe(false); + }); + + it("allows dev login in production only with the explicit ALLOW_DEV_LOGIN=1 escape hatch", () => { + vi.stubEnv("NODE_ENV", "production"); + vi.stubEnv("ALLOW_DEV_LOGIN", "1"); expect(isDevLoginAllowed()).toBe(true); }); + + it("ignores ALLOW_DEV_LOGIN values other than \"1\"", () => { + vi.stubEnv("NODE_ENV", "production"); + vi.stubEnv("ALLOW_DEV_LOGIN", "true"); + expect(isDevLoginAllowed()).toBe(false); + }); }); describe("authConfig.callbacks.jwt", () => { diff --git a/src/lib/auth/auth.config.ts b/src/lib/auth/auth.config.ts index 17274ad..488bb54 100644 --- a/src/lib/auth/auth.config.ts +++ b/src/lib/auth/auth.config.ts @@ -83,8 +83,17 @@ export function logGoogleAuthDiagnostics(): void { ); } +/** + * The dev credentials provider hands out ADMIN sessions to any email, so it + * must never be reachable on a public deployment. It is enabled only in + * `next dev`, or when `ALLOW_DEV_LOGIN=1` is set explicitly (local prod-build + * smoke tests). Missing Google credentials no longer enable it implicitly — + * that combination previously exposed admin sign-in on production. + */ export function isDevLoginAllowed(): boolean { - return process.env.NODE_ENV === "development" || !isGoogleAuthConfigured(); + return ( + process.env.NODE_ENV === "development" || process.env.ALLOW_DEV_LOGIN === "1" + ); } logGoogleAuthDiagnostics(); diff --git a/src/lib/storage/upload-limits.ts b/src/lib/storage/upload-limits.ts index 0e5a11f..feeee3a 100644 --- a/src/lib/storage/upload-limits.ts +++ b/src/lib/storage/upload-limits.ts @@ -16,9 +16,10 @@ export const ALLOWED_RESUME_MIME = [ "text/csv", "application/csv", "application/vnd.ms-excel", + "text/plain", ] as const; -export const ALLOWED_RESUME_EXTENSIONS = [".pdf", ".doc", ".docx", ".csv"] as const; +export const ALLOWED_RESUME_EXTENSIONS = [".pdf", ".doc", ".docx", ".csv", ".txt"] as const; export function hasAllowedResumeExtension(filename: string): boolean { const normalized = filename.toLowerCase(); diff --git a/src/schemas/application.ts b/src/schemas/application.ts index 529f5eb..4834a0c 100644 --- a/src/schemas/application.ts +++ b/src/schemas/application.ts @@ -25,7 +25,7 @@ export const resumeFileSchema = z if (!isAllowedResumeMime(value.type) && !hasAllowedResumeExtension(value.name ?? "")) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "Resume must be a PDF, DOC, DOCX, or CSV file", + message: "Resume must be a PDF, DOC, DOCX, TXT, or CSV file", }); } }); From 2ce90f4938532e23addf386b7002bd708ade8450 Mon Sep 17 00:00:00 2001 From: Richard Pillaca Date: Thu, 11 Jun 2026 14:45:59 -0500 Subject: [PATCH 4/4] feat(ui): route-level loading skeletons and action feedback toasts - Add loading.tsx fallbacks for every admin segment (shared AdminPageSkeleton), the public careers job page, and the root job board, so navigation shows immediate skeleton/branded loading states instead of a frozen screen while server components stream. - Add a shadcn-style Skeleton primitive. - RescoreButton: surface success/error via toast (failures were silently swallowed); RetryParsingButton: toast on success/failure in addition to the inline error text. Co-Authored-By: Claude --- .../(admin)/_components/AdminPageSkeleton.tsx | 22 ++++++++++++++++ src/app/(admin)/admin/applicants/loading.tsx | 5 ++++ .../(admin)/admin/email-templates/loading.tsx | 5 ++++ .../(admin)/admin/integrations/loading.tsx | 5 ++++ .../_components/RescoreButton.tsx | 17 ++++++++++-- .../_components/RetryParsingButton.tsx | 4 +++ .../[id]/applicants/[applicantId]/loading.tsx | 5 ++++ .../admin/jobs/[id]/applicants/loading.tsx | 5 ++++ src/app/(admin)/admin/jobs/[id]/loading.tsx | 5 ++++ src/app/(admin)/admin/jobs/loading.tsx | 5 ++++ src/app/(admin)/admin/loading.tsx | 5 ++++ .../(admin)/admin/notifications/loading.tsx | 5 ++++ src/app/(admin)/admin/settings/loading.tsx | 5 ++++ src/app/(admin)/admin/templates/loading.tsx | 5 ++++ src/app/(public)/careers/[slug]/loading.tsx | 26 +++++++++++++++++++ src/app/loading.tsx | 26 +++++++++++++++++++ src/components/ui/skeleton.tsx | 13 ++++++++++ 17 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 src/app/(admin)/_components/AdminPageSkeleton.tsx create mode 100644 src/app/(admin)/admin/applicants/loading.tsx create mode 100644 src/app/(admin)/admin/email-templates/loading.tsx create mode 100644 src/app/(admin)/admin/integrations/loading.tsx create mode 100644 src/app/(admin)/admin/jobs/[id]/applicants/[applicantId]/loading.tsx create mode 100644 src/app/(admin)/admin/jobs/[id]/applicants/loading.tsx create mode 100644 src/app/(admin)/admin/jobs/[id]/loading.tsx create mode 100644 src/app/(admin)/admin/jobs/loading.tsx create mode 100644 src/app/(admin)/admin/loading.tsx create mode 100644 src/app/(admin)/admin/notifications/loading.tsx create mode 100644 src/app/(admin)/admin/settings/loading.tsx create mode 100644 src/app/(admin)/admin/templates/loading.tsx create mode 100644 src/app/(public)/careers/[slug]/loading.tsx create mode 100644 src/app/loading.tsx create mode 100644 src/components/ui/skeleton.tsx diff --git a/src/app/(admin)/_components/AdminPageSkeleton.tsx b/src/app/(admin)/_components/AdminPageSkeleton.tsx new file mode 100644 index 0000000..849f36d --- /dev/null +++ b/src/app/(admin)/_components/AdminPageSkeleton.tsx @@ -0,0 +1,22 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +/** + * Generic route-segment fallback for admin pages: a header row plus a few + * card-shaped blocks. Shown by Next.js while the server component streams. + */ +export function AdminPageSkeleton({ cards = 3 }: { cards?: number }) { + return ( +
+
+ + +
+
+ {Array.from({ length: cards }).map((_, i) => ( + + ))} +
+ +
+ ); +} diff --git a/src/app/(admin)/admin/applicants/loading.tsx b/src/app/(admin)/admin/applicants/loading.tsx new file mode 100644 index 0000000..480c8d3 --- /dev/null +++ b/src/app/(admin)/admin/applicants/loading.tsx @@ -0,0 +1,5 @@ +import { AdminPageSkeleton } from "@/app/(admin)/_components/AdminPageSkeleton"; + +export default function Loading() { + return ; +} diff --git a/src/app/(admin)/admin/email-templates/loading.tsx b/src/app/(admin)/admin/email-templates/loading.tsx new file mode 100644 index 0000000..480c8d3 --- /dev/null +++ b/src/app/(admin)/admin/email-templates/loading.tsx @@ -0,0 +1,5 @@ +import { AdminPageSkeleton } from "@/app/(admin)/_components/AdminPageSkeleton"; + +export default function Loading() { + return ; +} diff --git a/src/app/(admin)/admin/integrations/loading.tsx b/src/app/(admin)/admin/integrations/loading.tsx new file mode 100644 index 0000000..480c8d3 --- /dev/null +++ b/src/app/(admin)/admin/integrations/loading.tsx @@ -0,0 +1,5 @@ +import { AdminPageSkeleton } from "@/app/(admin)/_components/AdminPageSkeleton"; + +export default function Loading() { + return ; +} diff --git a/src/app/(admin)/admin/jobs/[id]/applicants/[applicantId]/_components/RescoreButton.tsx b/src/app/(admin)/admin/jobs/[id]/applicants/[applicantId]/_components/RescoreButton.tsx index 2f35eff..cc6fa56 100644 --- a/src/app/(admin)/admin/jobs/[id]/applicants/[applicantId]/_components/RescoreButton.tsx +++ b/src/app/(admin)/admin/jobs/[id]/applicants/[applicantId]/_components/RescoreButton.tsx @@ -3,6 +3,7 @@ import { useTransition } from "react"; import { useRouter } from "next/navigation"; import { Loader2, Sparkles } from "lucide-react"; +import { toast } from "sonner"; interface RescoreButtonProps { applicantId: string; @@ -15,8 +16,20 @@ export function RescoreButton({ applicantId, disabled }: RescoreButtonProps) { function handleClick() { startTransition(async () => { - await fetch(`/api/admin/applicants/${applicantId}/rescore`, { method: "POST" }); - router.refresh(); + try { + const response = await fetch(`/api/admin/applicants/${applicantId}/rescore`, { + method: "POST", + }); + if (!response.ok) { + const body = (await response.json().catch(() => null)) as { error?: string } | null; + toast.error(body?.error ?? "Re-scoring failed."); + return; + } + toast.success("Match score updated."); + router.refresh(); + } catch { + toast.error("Re-scoring failed. Check your connection and try again."); + } }); } diff --git a/src/app/(admin)/admin/jobs/[id]/applicants/[applicantId]/_components/RetryParsingButton.tsx b/src/app/(admin)/admin/jobs/[id]/applicants/[applicantId]/_components/RetryParsingButton.tsx index 7cf126e..c750414 100644 --- a/src/app/(admin)/admin/jobs/[id]/applicants/[applicantId]/_components/RetryParsingButton.tsx +++ b/src/app/(admin)/admin/jobs/[id]/applicants/[applicantId]/_components/RetryParsingButton.tsx @@ -4,6 +4,7 @@ import { useTransition } from "react"; import { useRouter } from "next/navigation"; import { RefreshCw, Loader2 } from "lucide-react"; import { useState } from "react"; +import { toast } from "sonner"; interface RetryParsingButtonProps { applicantId: string; @@ -26,6 +27,9 @@ export function RetryParsingButton({ applicantId, status }: RetryParsingButtonPr const body = (await response.json().catch(() => null)) as { error?: string } | null; if (!response.ok) { setError(body?.error ?? "Resume parsing failed."); + toast.error(body?.error ?? "Resume parsing failed."); + } else { + toast.success("Resume parsed. Refreshing applicant data…"); } router.refresh(); }); diff --git a/src/app/(admin)/admin/jobs/[id]/applicants/[applicantId]/loading.tsx b/src/app/(admin)/admin/jobs/[id]/applicants/[applicantId]/loading.tsx new file mode 100644 index 0000000..480c8d3 --- /dev/null +++ b/src/app/(admin)/admin/jobs/[id]/applicants/[applicantId]/loading.tsx @@ -0,0 +1,5 @@ +import { AdminPageSkeleton } from "@/app/(admin)/_components/AdminPageSkeleton"; + +export default function Loading() { + return ; +} diff --git a/src/app/(admin)/admin/jobs/[id]/applicants/loading.tsx b/src/app/(admin)/admin/jobs/[id]/applicants/loading.tsx new file mode 100644 index 0000000..480c8d3 --- /dev/null +++ b/src/app/(admin)/admin/jobs/[id]/applicants/loading.tsx @@ -0,0 +1,5 @@ +import { AdminPageSkeleton } from "@/app/(admin)/_components/AdminPageSkeleton"; + +export default function Loading() { + return ; +} diff --git a/src/app/(admin)/admin/jobs/[id]/loading.tsx b/src/app/(admin)/admin/jobs/[id]/loading.tsx new file mode 100644 index 0000000..480c8d3 --- /dev/null +++ b/src/app/(admin)/admin/jobs/[id]/loading.tsx @@ -0,0 +1,5 @@ +import { AdminPageSkeleton } from "@/app/(admin)/_components/AdminPageSkeleton"; + +export default function Loading() { + return ; +} diff --git a/src/app/(admin)/admin/jobs/loading.tsx b/src/app/(admin)/admin/jobs/loading.tsx new file mode 100644 index 0000000..480c8d3 --- /dev/null +++ b/src/app/(admin)/admin/jobs/loading.tsx @@ -0,0 +1,5 @@ +import { AdminPageSkeleton } from "@/app/(admin)/_components/AdminPageSkeleton"; + +export default function Loading() { + return ; +} diff --git a/src/app/(admin)/admin/loading.tsx b/src/app/(admin)/admin/loading.tsx new file mode 100644 index 0000000..480c8d3 --- /dev/null +++ b/src/app/(admin)/admin/loading.tsx @@ -0,0 +1,5 @@ +import { AdminPageSkeleton } from "@/app/(admin)/_components/AdminPageSkeleton"; + +export default function Loading() { + return ; +} diff --git a/src/app/(admin)/admin/notifications/loading.tsx b/src/app/(admin)/admin/notifications/loading.tsx new file mode 100644 index 0000000..480c8d3 --- /dev/null +++ b/src/app/(admin)/admin/notifications/loading.tsx @@ -0,0 +1,5 @@ +import { AdminPageSkeleton } from "@/app/(admin)/_components/AdminPageSkeleton"; + +export default function Loading() { + return ; +} diff --git a/src/app/(admin)/admin/settings/loading.tsx b/src/app/(admin)/admin/settings/loading.tsx new file mode 100644 index 0000000..480c8d3 --- /dev/null +++ b/src/app/(admin)/admin/settings/loading.tsx @@ -0,0 +1,5 @@ +import { AdminPageSkeleton } from "@/app/(admin)/_components/AdminPageSkeleton"; + +export default function Loading() { + return ; +} diff --git a/src/app/(admin)/admin/templates/loading.tsx b/src/app/(admin)/admin/templates/loading.tsx new file mode 100644 index 0000000..480c8d3 --- /dev/null +++ b/src/app/(admin)/admin/templates/loading.tsx @@ -0,0 +1,5 @@ +import { AdminPageSkeleton } from "@/app/(admin)/_components/AdminPageSkeleton"; + +export default function Loading() { + return ; +} diff --git a/src/app/(public)/careers/[slug]/loading.tsx b/src/app/(public)/careers/[slug]/loading.tsx new file mode 100644 index 0000000..2c8c390 --- /dev/null +++ b/src/app/(public)/careers/[slug]/loading.tsx @@ -0,0 +1,26 @@ +export default function Loading() { + return ( +
+
+
+ SL +
+

Loading position…

+
+
+ ); +} diff --git a/src/app/loading.tsx b/src/app/loading.tsx new file mode 100644 index 0000000..fb6d440 --- /dev/null +++ b/src/app/loading.tsx @@ -0,0 +1,26 @@ +export default function Loading() { + return ( +
+
+
+ SL +
+

Loading…

+
+
+ ); +} diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..e8a07f1 --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/lib/utils/cn"; + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { Skeleton };