From b8faf752e975c3be2bdec6196945b811eed605e2 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Wed, 8 Apr 2026 22:40:43 +0200 Subject: [PATCH] feat: align external dashboards, profile settings split, and role flows --- src/components/PublicHeader.tsx | 4 +- .../admin/DashboardDesignPreview.tsx | 28 +- .../dashboard/CompanyApplicationsPage.tsx | 320 ++++++++++++++++ src/components/dashboard/CompanyJobsPage.tsx | 351 ++++++++++++++++++ .../CompanyShortlistedCandidatesPage.tsx | 109 ++++++ src/components/dashboard/CreditsPage.tsx | 118 ++++++ .../dashboard/CustomerRequirementsPage.tsx | 217 +++++++++++ .../dashboard/CustomerResponsesPage.tsx | 223 +++++++++++ .../dashboard/ExploreServicesPage.tsx | 46 +++ .../dashboard/HelpCenterDashboardPage.tsx | 143 +++++++ .../dashboard/JobSeekerApplicationsPage.tsx | 133 +++++++ .../dashboard/JobSeekerJobsPage.tsx | 148 ++++++++ .../dashboard/JobSeekerSavedJobsPage.tsx | 92 +++++ src/components/dashboard/LogoutPage.tsx | 69 ++++ src/components/dashboard/MyDashboardPage.tsx | 162 ++++++++ .../dashboard/ProfessionalLeadsPage.tsx | 137 +++++++ .../dashboard/ProfessionalResponsesPage.tsx | 107 ++++++ src/components/dashboard/ProfilePage.tsx | 79 ++-- .../dashboard/RoleDashboardShared.ts | 44 +++ src/components/dashboard/SettingsPage.tsx | 297 +++++++++++++++ .../dashboard/SwitchServicesPage.tsx | 231 ++++++++++++ src/lib/api.ts | 4 +- src/lib/auth-intent.test.ts | 10 +- src/lib/auth-intent.ts | 8 +- src/lib/auth.tsx | 5 +- src/routes/dashboard.tsx | 153 +++++++- src/routes/login.tsx | 1 - src/routes/signup.tsx | 3 +- 28 files changed, 3146 insertions(+), 96 deletions(-) create mode 100644 src/components/dashboard/CompanyApplicationsPage.tsx create mode 100644 src/components/dashboard/CompanyJobsPage.tsx create mode 100644 src/components/dashboard/CompanyShortlistedCandidatesPage.tsx create mode 100644 src/components/dashboard/CreditsPage.tsx create mode 100644 src/components/dashboard/CustomerRequirementsPage.tsx create mode 100644 src/components/dashboard/CustomerResponsesPage.tsx create mode 100644 src/components/dashboard/ExploreServicesPage.tsx create mode 100644 src/components/dashboard/HelpCenterDashboardPage.tsx create mode 100644 src/components/dashboard/JobSeekerApplicationsPage.tsx create mode 100644 src/components/dashboard/JobSeekerJobsPage.tsx create mode 100644 src/components/dashboard/JobSeekerSavedJobsPage.tsx create mode 100644 src/components/dashboard/LogoutPage.tsx create mode 100644 src/components/dashboard/MyDashboardPage.tsx create mode 100644 src/components/dashboard/ProfessionalLeadsPage.tsx create mode 100644 src/components/dashboard/ProfessionalResponsesPage.tsx create mode 100644 src/components/dashboard/RoleDashboardShared.ts create mode 100644 src/components/dashboard/SettingsPage.tsx create mode 100644 src/components/dashboard/SwitchServicesPage.tsx diff --git a/src/components/PublicHeader.tsx b/src/components/PublicHeader.tsx index 66331bf..150c37a 100644 --- a/src/components/PublicHeader.tsx +++ b/src/components/PublicHeader.tsx @@ -41,7 +41,7 @@ export default function PublicHeader() { + + + +
+ {actionMsg()} +
+
+ + +
Loading jobs…
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ Create a job first to view applications. +
+
+
+ + +
Loading applications…
+
+ + +
No applications found for this job.
+
+ + 0}> +
+ + {(app) => ( +
+
+
+

+ Application #{app.id.slice(0, 8)} +

+

+ Applied at {prettyDate(app.applied_at)} +

+
+ + {app.status.replace(/_/g, ' ')} + +
+ + +

+ {app.cover_letter} +

+
+ + +

+ + View Resume + +

+
+ +
+ + + + + + +
+ + +
+

+ {contactByApp()[app.id]?.full_name || 'Applicant'} +

+

+ {contactByApp()[app.id]?.email || 'No email'} +

+

+ {contactByApp()[app.id]?.phone || 'No phone'} +

+

+ Remaining contact views: {contactByApp()[app.id]?.quota?.total_remaining ?? 0} +

+
+
+
+ )} +
+
+
+ + ); +} diff --git a/src/components/dashboard/CompanyJobsPage.tsx b/src/components/dashboard/CompanyJobsPage.tsx new file mode 100644 index 0000000..83ca446 --- /dev/null +++ b/src/components/dashboard/CompanyJobsPage.tsx @@ -0,0 +1,351 @@ +import { For, Show, createSignal, onMount } from 'solid-js'; +import { BTN_GHOST, BTN_ORANGE, BTN_PRIMARY, CARD, INPUT, LABEL } from '~/components/DashboardShell'; + +const API = '/api/gateway'; + +interface JobItem { + id: string; + title: string; + description: string; + location: string; + job_type: string; + status: string; + category?: string | null; + salary_min?: number | null; + salary_max?: number | null; + experience_years?: number | null; + skills?: string[] | null; + created_at?: string; +} + +interface JobFormState { + title: string; + category: string; + description: string; + location: string; + job_type: string; + salary_min: string; + salary_max: string; + experience_years: string; + skills: string; +} + +const EMPTY_FORM: JobFormState = { + title: '', + category: '', + description: '', + location: '', + job_type: 'FULL_TIME', + salary_min: '', + salary_max: '', + experience_years: '', + skills: '', +}; + +async function apiFetch(path: string, opts?: RequestInit) { + return fetch(`${API}${path}`, { + ...opts, + credentials: 'include', + headers: { 'Content-Type': 'application/json', ...(opts?.headers ?? {}) }, + }); +} + +export default function CompanyJobsPage() { + const [jobs, setJobs] = createSignal([]); + const [loading, setLoading] = createSignal(true); + const [showForm, setShowForm] = createSignal(false); + const [form, setForm] = createSignal({ ...EMPTY_FORM }); + const [saving, setSaving] = createSignal(false); + const [error, setError] = createSignal(''); + const [actionMsg, setActionMsg] = createSignal(''); + const [busyJobId, setBusyJobId] = createSignal(null); + + const loadJobs = async () => { + setLoading(true); + try { + const res = await apiFetch('/api/companies/jobs?page=1&limit=50'); + if (!res.ok) { + setJobs([]); + return; + } + const payload = await res.json().catch(() => ({})); + setJobs(Array.isArray(payload?.data) ? payload.data : []); + } finally { + setLoading(false); + } + }; + + onMount(loadJobs); + + const setField = (key: keyof JobFormState, val: string) => + setForm((prev) => ({ ...prev, [key]: val })); + + const openCreate = () => { + setForm({ ...EMPTY_FORM }); + setError(''); + setActionMsg(''); + setShowForm(true); + }; + + const closeCreate = () => { + setShowForm(false); + setForm({ ...EMPTY_FORM }); + setError(''); + }; + + const handleCreate = async () => { + if (!form().title.trim() || !form().description.trim() || !form().location.trim()) { + setError('Title, description, and location are required.'); + return; + } + + setSaving(true); + setError(''); + setActionMsg(''); + + const payload = { + title: form().title.trim(), + category: form().category.trim() || undefined, + description: form().description.trim(), + location: form().location.trim(), + job_type: form().job_type, + salary_min: form().salary_min ? Number(form().salary_min) : undefined, + salary_max: form().salary_max ? Number(form().salary_max) : undefined, + experience_years: form().experience_years ? Number(form().experience_years) : undefined, + skills: form().skills + .split(',') + .map((s) => s.trim()) + .filter(Boolean), + }; + + try { + const res = await apiFetch('/api/companies/jobs', { + method: 'POST', + body: JSON.stringify(payload), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + setError(data.error ?? data.message ?? 'Failed to create job.'); + return; + } + setActionMsg('Job created as draft.'); + closeCreate(); + await loadJobs(); + } catch { + setError('Network error. Please try again.'); + } finally { + setSaving(false); + } + }; + + const submitJob = async (jobId: string) => { + setBusyJobId(jobId); + setActionMsg(''); + try { + const res = await apiFetch(`/api/companies/jobs/${jobId}/submit`, { method: 'POST' }); + if (res.ok) { + setActionMsg('Job submitted for verification.'); + await loadJobs(); + } else { + const data = await res.json().catch(() => ({})); + setActionMsg(data.error ?? data.message ?? 'Unable to submit this job.'); + } + } finally { + setBusyJobId(null); + } + }; + + const closeJob = async (jobId: string) => { + setBusyJobId(jobId); + setActionMsg(''); + try { + const res = await apiFetch(`/api/companies/jobs/${jobId}/close`, { method: 'POST' }); + if (res.ok) { + setActionMsg('Job closed.'); + await loadJobs(); + } else { + const data = await res.json().catch(() => ({})); + setActionMsg(data.error ?? data.message ?? 'Unable to close this job.'); + } + } finally { + setBusyJobId(null); + } + }; + + const statusColor = (status: string) => { + switch (status) { + case 'DRAFT': return '#6B7280'; + case 'PENDING_APPROVAL': return '#F59E0B'; + case 'LIVE': return '#10B981'; + case 'REJECTED': return '#EF4444'; + case 'CLOSED': return '#374151'; + default: return '#6B7280'; + } + }; + + return ( +
+
+
+

Jobs

+

+ Create and manage your job postings. +

+
+ +
+ + +
+ {actionMsg()} +
+
+ + +
+

+ New Job +

+
+
+ + setField('title', e.currentTarget.value)} style={INPUT} placeholder="Frontend Developer" /> +
+
+ + setField('category', e.currentTarget.value)} style={INPUT} placeholder="Engineering" /> +
+
+ + +
+
+ + setField('location', e.currentTarget.value)} style={INPUT} placeholder="Bengaluru (Hybrid)" /> +
+
+ +