From 6c83fb6af74e92cb65c515b46f5325c84e4f690a Mon Sep 17 00:00:00 2001 From: Gabriel Ramos Date: Wed, 10 Jun 2026 14:26:50 -0400 Subject: [PATCH] feat(theme): add dark mode & move language toggle --- app/(dashboard)/candidates/page.tsx | 179 ++++++------ app/(dashboard)/interviews/page.tsx | 119 +++++--- app/(dashboard)/jobs/page.tsx | 432 ++++++++++++++++++---------- app/(dashboard)/layout.tsx | 102 ++++++- app/(dashboard)/workflows/page.tsx | 100 +++++-- app/layout.tsx | 5 +- components/AppContext.tsx | 79 +++++ tailwind.config.ts | 7 + 8 files changed, 705 insertions(+), 318 deletions(-) create mode 100644 components/AppContext.tsx diff --git a/app/(dashboard)/candidates/page.tsx b/app/(dashboard)/candidates/page.tsx index ff25da4..4444be7 100644 --- a/app/(dashboard)/candidates/page.tsx +++ b/app/(dashboard)/candidates/page.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useState, useEffect, useRef, useCallback } from "react"; +import { useApp } from "@/components/AppContext"; interface Score { id: string; @@ -215,7 +216,7 @@ export default function CandidatesPage() { const searchInputRef = useRef(null); // i18n Language Toggle State - const [lang, setLang] = useState<"en" | "es">("en"); + const { lang } = useApp(); const t = translations[lang]; // Delete & Duplicate States @@ -479,11 +480,11 @@ export default function CandidatesPage() { return (
{/* CV Uploader (Vacuum Ingestion) */} -
-

+
+

{t.uploadCv}

-

+

{t.uploadCvDesc}

{uploadStatuses.length > 0 && ( -
-

+
+

{t.uploadProgress}

-
    +
      {uploadStatuses.map((item, idx) => (
    • - + {item.name} {item.status === "uploading" && ( - + {lang === "es" ? "Cargando..." : "Uploading..."} )} {item.status === "success" && ( - + ✓ {t.success} )} {item.status === "error" && ( - + ✗ {t.error} )}
      {item.errorMessage && ( -

      {item.errorMessage}

      +

      {item.errorMessage}

      )}
    • ))} @@ -538,29 +539,23 @@ export default function CandidatesPage() {
{loading ? ( -
-

{t.loading}

+
+

{t.loading}

) : candidates.length === 0 ? ( -
-

+

+

{t.noCandidatesFound}

) : (
{/* LEFT COLUMN: Sidebar (1/3 width) */} -
-
-

+
+
+

{t.candidatesTitle}

-
{/* Search Input */} @@ -571,13 +566,13 @@ export default function CandidatesPage() { value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder={t.searchPlaceholder} - className="w-full px-3 py-2 border border-slate-200 rounded-md text-slate-900 bg-white placeholder:text-slate-500 text-sm focus:outline-none" + className="w-full px-3 py-2 border border-slate-200 dark:border-slate-700 rounded-md text-slate-900 dark:text-white bg-white dark:bg-slate-800 placeholder:text-slate-500 dark:placeholder:text-slate-400 text-sm focus:outline-none" /> {/* Search Quick Actions (only show when searchQuery !== "") */} {searchQuery !== "" && (
- + {t.searchBy}: ))} {sortedCandidates.length === 0 && ( -

+

{lang === "es" ? "No se encontraron candidatos" : "No candidates found"}

)} @@ -645,25 +640,25 @@ export default function CandidatesPage() { {/* RIGHT COLUMN: Detail Pane (2/3 width) */}
{selectedCandidate ? ( -
+
{/* Header Profile Details */} -
+
-

+

{selectedCandidate.name}

-

+

{t.createdDate}: {new Date(selectedCandidate.created_at).toLocaleDateString()}

- {t.email}: - {selectedCandidate.contact_info.email} + {t.email}: + {selectedCandidate.contact_info.email}
- {t.phone}: - {selectedCandidate.contact_info.phone || "N/A"} + {t.phone}: + {selectedCandidate.contact_info.phone || "N/A"}
@@ -679,10 +674,10 @@ export default function CandidatesPage() { {/* Summary Extracted */} {selectedCandidate.contact_info.summary && (
-

+

{t.professionalSummary}

-

+

{selectedCandidate.contact_info.summary}

@@ -691,14 +686,14 @@ export default function CandidatesPage() { {/* Skills tag group */} {selectedCandidate.contact_info.skills && selectedCandidate.contact_info.skills.length > 0 && (
-

+

{t.skillsAndTech}

{selectedCandidate.contact_info.skills.map((skill) => ( {skill} @@ -709,37 +704,37 @@ export default function CandidatesPage() { {/* Linked Vacancies Section */}
-

+

{t.linkedVacancies}

{linkedVacancies.length > 0 ? ( -
+
- + - + {linkedVacancies.map((vacancy) => ( - - + -
{t.jobTitle} {t.aiScore} {t.classification} {t.stage}
+
{vacancy.jobTitle} + {vacancy.aiScore !== null ? `${vacancy.aiScore} / 100` : "-"} {vacancy.classification ? ( {translateClassification(vacancy.classification, lang)} @@ -749,7 +744,7 @@ export default function CandidatesPage() { {vacancy.stage ? ( - + {translateStage(vacancy.stage, lang)} ) : ( @@ -762,7 +757,7 @@ export default function CandidatesPage() {
) : ( -

+

{t.noLinkedVacancies}

)} @@ -770,8 +765,8 @@ export default function CandidatesPage() {
) : ( -
-

+

+

{t.noCandidateSelected}

@@ -782,18 +777,18 @@ export default function CandidatesPage() { {/* Delete Confirmation Modal */} {showDeleteConfirm && ( -
-
-

+
+
+

{t.confirmTitle}

-

+

{t.deleteConfirmation}

@@ -810,55 +805,55 @@ export default function CandidatesPage() { {/* Duplicate Detection dialog */} {duplicateData && ( -
-
+
+
-

+

{t.duplicateDetected}

-

+

{t.duplicateMsg} ({duplicateData.fileName})

{/* Existing Profile */} -
-

+
+

{t.existingProfile}

-
+
{duplicateData.existingCandidate.name}
-
- Email: {duplicateData.existingCandidate.contact_info.email} +
+ Email: {duplicateData.existingCandidate.contact_info.email}
-
- Phone: {duplicateData.existingCandidate.contact_info.phone || "N/A"} +
+ Phone: {duplicateData.existingCandidate.contact_info.phone || "N/A"}
{duplicateData.existingCandidate.contact_info.summary && ( -

+

{duplicateData.existingCandidate.contact_info.summary}

)}
{/* New Profile */} -
-

+
+

{t.newProfile}

-
+
{duplicateData.newProfile.candidateName || duplicateData.newProfile.name || "Unknown"}
-
- Email: {duplicateData.newProfile.email || "N/A"} +
+ Email: {duplicateData.newProfile.email || "N/A"}
-
- Phone: {duplicateData.newProfile.phone || "N/A"} +
+ Phone: {duplicateData.newProfile.phone || "N/A"}
{duplicateData.newProfile.summary && ( -

+

{duplicateData.newProfile.summary}

)} @@ -866,26 +861,26 @@ export default function CandidatesPage() {
{/* AI Comparison Summary */} -
-

+
+

{t.aiComparison}

{duplicateData.comparison ? ( -

+

{getBilingualText(duplicateData.comparison, lang)}

) : ( -

+

{lang === "es" ? "Comparando perfiles con IA..." : "Comparing profiles with AI..."}

)}
{/* Actions */} -
+
diff --git a/app/(dashboard)/interviews/page.tsx b/app/(dashboard)/interviews/page.tsx index adc0583..b780739 100644 --- a/app/(dashboard)/interviews/page.tsx +++ b/app/(dashboard)/interviews/page.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react"; import { supabase } from "@/lib/supabase"; +import { useApp } from "@/components/AppContext"; interface Interview { id: string; @@ -19,7 +20,59 @@ interface Interview { } | null; } +const translations = { + en: { + interviewsTitle: "Interviews", + interviewsSubtitle: "Manage scheduled candidate interview stages and write evaluation feedback.", + loadingInterviews: "Loading interviews...", + noInterviews: "No interviews scheduled. Upload candidate CVs under the Jobs tab to trigger evaluations.", + unknownCandidate: "Unknown Candidate", + unknownJob: "Unknown Job", + role: "Role", + dateScheduled: "Date Scheduled", + stage: "Stage", + interviewFeedback: "Interview Feedback", + feedbackPlaceholder: "Write detailed assessment feedback, questions, or observations...", + saving: "Saving...", + updateInterview: "Update Interview", + updateSuccess: "Interview updated successfully!", + updateFailed: "Failed to update interview.", + screening: "Screening", + technical: "Technical", + cultural: "Cultural", + offer: "Offer", + hired: "Hired", + rejected: "Rejected" + }, + es: { + interviewsTitle: "Entrevistas", + interviewsSubtitle: "Gestione las etapas de entrevistas programadas de los candidatos y escriba comentarios de evaluación.", + loadingInterviews: "Cargando entrevistas...", + noInterviews: "No hay entrevistas programadas. Cargue los CV de los candidatos en la pestaña Vacantes para activar las evaluaciones.", + unknownCandidate: "Candidato Desconocido", + unknownJob: "Puesto Desconocido", + role: "Puesto", + dateScheduled: "Fecha Programada", + stage: "Etapa", + interviewFeedback: "Comentarios de la Entrevista", + feedbackPlaceholder: "Escriba comentarios detallados de la evaluación, preguntas u observaciones...", + saving: "Guardando...", + updateInterview: "Actualizar Entrevista", + updateSuccess: "¡Entrevista actualizada con éxito!", + updateFailed: "Error al actualizar la entrevista.", + screening: "Preselección", + technical: "Técnica", + cultural: "Cultural", + offer: "Oferta", + hired: "Contratado", + rejected: "Rechazado" + } +}; + export default function InterviewsPage() { + const { lang } = useApp(); + const t = translations[lang]; + const [interviews, setInterviews] = useState([]); const [loading, setLoading] = useState(true); const [updatingId, setUpdatingId] = useState(null); @@ -97,7 +150,7 @@ export default function InterviewsPage() { if (error) throw error; - setActionMessage("Interview updated successfully!"); + setActionMessage(t.updateSuccess); // Hide message after 3 seconds setTimeout(() => setActionMessage(null), 3000); @@ -111,7 +164,7 @@ export default function InterviewsPage() { ); } catch (err: unknown) { console.error("Error updating interview:", err); - setActionMessage("Failed to update interview."); + setActionMessage(t.updateFailed); } finally { setUpdatingId(null); } @@ -121,26 +174,26 @@ export default function InterviewsPage() {
-

Interviews

-

- Manage scheduled candidate interview stages and write evaluation feedback. +

{t.interviewsTitle}

+

+ {t.interviewsSubtitle}

{actionMessage && ( -
+
{actionMessage}
)}
{loading ? ( -
-

Loading interviews...

+
+

{t.loadingInterviews}

) : interviews.length === 0 ? ( -
-

- No interviews scheduled. Upload candidate CVs under the Jobs tab to trigger evaluations. +

+

+ {t.noInterviews}

) : ( @@ -154,47 +207,47 @@ export default function InterviewsPage() { return (
{/* Header */} -
+
-

- {interview.candidates?.name || "Unknown Candidate"} +

+ {interview.candidates?.name || t.unknownCandidate}

-

- Role: {interview.jobs?.title || "Unknown Job"} +

+ {t.role}: {interview.jobs?.title || t.unknownJob}

-

- Date Scheduled:{" "} +

+ {t.dateScheduled}:{" "} {new Date(interview.interview_date).toLocaleString()}

-
{/* Feedback Area */}
-