-
Explore Nxtgauge
-
- Discover available services and role journeys across the platform.
+
Explore Nxtgauge
+
+ Runtime-driven service catalog based on your account and active role configuration.
-
-
Available Services
-
-
- {(name) => (
-
-
{name}
-
Explore this role in dashboard and management flows.
-
- )}
-
+
+
+ {msg()}
+
+
+
+ {err()}
+
+
+
+
+
+
Available Services
+
+
+
+
+ Loading services...
+
+
+
+ No services returned by runtime configuration.
+
+
+
0}>
+
+
+ {(card) => (
+
+
{card.title}
+
{card.subtitle}
+
+
+ )}
+
+
+
);
}
-
diff --git a/src/components/dashboard/JobSeekerJobsPage.tsx b/src/components/dashboard/JobSeekerJobsPage.tsx
index 848749f..597440a 100644
--- a/src/components/dashboard/JobSeekerJobsPage.tsx
+++ b/src/components/dashboard/JobSeekerJobsPage.tsx
@@ -1,5 +1,6 @@
import { For, Show, createSignal, onMount } from 'solid-js';
import { BTN_GHOST, BTN_ORANGE, CARD, INPUT } from '~/components/DashboardShell';
+import { readJobSeekerProfile, updateJobSeekerCustomData } from '~/lib/job-seeker-custom-data';
const API = '/api/gateway';
@@ -15,6 +16,15 @@ type JobItem = {
description?: string | null;
};
+type SavedJob = {
+ id: string;
+ title: string;
+ company?: string;
+ location?: string;
+ salary?: string;
+ saved_at: string;
+};
+
async function apiFetch(path: string, opts?: RequestInit) {
return fetch(`${API}${path}`, {
...opts,
@@ -23,8 +33,36 @@ async function apiFetch(path: string, opts?: RequestInit) {
});
}
+function formatSalary(job: JobItem): string {
+ const min = Number(job.salary_min || 0);
+ const max = Number(job.salary_max || 0);
+ if (!min && !max) return '';
+ if (min && max) return `${min} - ${max}`;
+ return String(min || max || '');
+}
+
+function toSavedJobs(value: unknown): SavedJob[] {
+ if (!Array.isArray(value)) return [];
+ return value
+ .map((item) => {
+ const row = item as Record
;
+ const id = String(row?.id || '').trim();
+ if (!id) return null;
+ return {
+ id,
+ title: String(row?.title || 'Untitled Job'),
+ company: String(row?.company || row?.company_name || ''),
+ location: String(row?.location || ''),
+ salary: String(row?.salary || ''),
+ saved_at: String(row?.saved_at || new Date().toISOString()),
+ };
+ })
+ .filter(Boolean) as SavedJob[];
+}
+
export default function JobSeekerJobsPage() {
const [rows, setRows] = createSignal([]);
+ const [savedJobs, setSavedJobs] = createSignal([]);
const [loading, setLoading] = createSignal(true);
const [busyId, setBusyId] = createSignal(null);
const [search, setSearch] = createSignal('');
@@ -51,7 +89,20 @@ export default function JobSeekerJobsPage() {
}
};
- onMount(loadRows);
+ const loadSavedJobs = async () => {
+ try {
+ const profile = await readJobSeekerProfile();
+ setSavedJobs(toSavedJobs(profile?.custom_data?.saved_jobs));
+ } catch {
+ // non-blocking
+ setSavedJobs([]);
+ }
+ };
+
+ onMount(() => {
+ void loadRows();
+ void loadSavedJobs();
+ });
const applyJob = async (jobId: string) => {
setBusyId(jobId);
@@ -75,6 +126,37 @@ export default function JobSeekerJobsPage() {
}
};
+ const isSaved = (jobId: string) => savedJobs().some((row) => row.id === jobId);
+
+ const toggleSave = async (job: JobItem) => {
+ setBusyId(job.id);
+ setMsg('');
+ setErr('');
+ const existing = savedJobs();
+ const next = isSaved(job.id)
+ ? existing.filter((row) => row.id !== job.id)
+ : [
+ ...existing,
+ {
+ id: job.id,
+ title: String(job.title || 'Untitled Job'),
+ company: String(job.company_name || ''),
+ location: String(job.location || ''),
+ salary: formatSalary(job),
+ saved_at: new Date().toISOString(),
+ },
+ ];
+ try {
+ await updateJobSeekerCustomData((current) => ({ ...current, saved_jobs: next }));
+ setSavedJobs(next);
+ setMsg(isSaved(job.id) ? 'Job removed from saved list.' : 'Job saved for later.');
+ } catch (e: any) {
+ setErr(e?.message || 'Failed to update saved jobs.');
+ } finally {
+ setBusyId(null);
+ }
+ };
+
const filtered = () => {
const q = search().trim().toLowerCase();
if (!q) return rows();
@@ -130,9 +212,17 @@ export default function JobSeekerJobsPage() {
{row.description || 'No description provided.'}
- Salary: {row.salary_min || 0} - {row.salary_max || 0}
+ Salary: {formatSalary(row) || 'Not specified'}
-
+
+
@@ -145,4 +235,3 @@ export default function JobSeekerJobsPage() {
);
}
-
diff --git a/src/components/dashboard/JobSeekerSavedJobsPage.tsx b/src/components/dashboard/JobSeekerSavedJobsPage.tsx
index edf6519..e06ea7c 100644
--- a/src/components/dashboard/JobSeekerSavedJobsPage.tsx
+++ b/src/components/dashboard/JobSeekerSavedJobsPage.tsx
@@ -1,7 +1,6 @@
import { For, Show, createSignal, onMount } from 'solid-js';
import { BTN_GHOST, CARD } from '~/components/DashboardShell';
-
-const STORAGE_KEY = 'nxtgauge_saved_jobs_v1';
+import { readJobSeekerProfile, updateJobSeekerCustomData } from '~/lib/job-seeker-custom-data';
type SavedJob = {
id: string;
@@ -12,36 +11,64 @@ type SavedJob = {
saved_at: string;
};
-function loadSaved(): SavedJob[] {
- try {
- const raw = window.localStorage.getItem(STORAGE_KEY);
- if (!raw) return [];
- const parsed = JSON.parse(raw);
- return Array.isArray(parsed) ? parsed : [];
- } catch {
- return [];
- }
+function toSavedJobs(value: unknown): SavedJob[] {
+ if (!Array.isArray(value)) return [];
+ return value
+ .map((item) => {
+ const row = item as Record
;
+ const id = String(row?.id || '').trim();
+ if (!id) return null;
+ return {
+ id,
+ title: String(row?.title || 'Untitled Job'),
+ company: String(row?.company || row?.company_name || ''),
+ location: String(row?.location || ''),
+ salary: String(row?.salary || ''),
+ saved_at: String(row?.saved_at || new Date().toISOString()),
+ };
+ })
+ .filter(Boolean) as SavedJob[];
}
-function persist(list: SavedJob[]) {
- try {
- window.localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
- } catch {
- // ignore
- }
+function renderSavedAt(value: string): string {
+ const dt = new Date(value);
+ if (Number.isNaN(dt.getTime())) return '—';
+ return dt.toLocaleString('en-IN');
}
export default function JobSeekerSavedJobsPage() {
const [rows, setRows] = createSignal([]);
+ const [loading, setLoading] = createSignal(true);
+ const [err, setErr] = createSignal('');
- onMount(() => {
- setRows(loadSaved());
- });
+ const loadRows = async () => {
+ setLoading(true);
+ setErr('');
+ try {
+ const profile = await readJobSeekerProfile();
+ setRows(toSavedJobs(profile?.custom_data?.saved_jobs));
+ } catch {
+ setErr('Failed to load saved jobs.');
+ setRows([]);
+ } finally {
+ setLoading(false);
+ }
+ };
- const removeRow = (id: string) => {
+ onMount(() => void loadRows());
+
+ const removeRow = async (id: string) => {
const next = rows().filter((row) => row.id !== id);
- setRows(next);
- persist(next);
+ setLoading(true);
+ setErr('');
+ try {
+ await updateJobSeekerCustomData((current) => ({ ...current, saved_jobs: next }));
+ setRows(next);
+ } catch (e: any) {
+ setErr(e?.message || 'Failed to remove saved job.');
+ } finally {
+ setLoading(false);
+ }
};
return (
@@ -49,19 +76,27 @@ export default function JobSeekerSavedJobsPage() {
Saved Jobs
- Jobs bookmarked for later. Saved locally on this device.
+ Jobs bookmarked for later.
+
+ {err()}
+
+
Bookmarked Jobs
-
+
-
+
+
+ Loading saved jobs...
+
+
No saved jobs yet.
- 0}>
+ 0}>
{(row) => (
@@ -73,10 +108,10 @@ export default function JobSeekerSavedJobsPage() {
{row.company || '—'} {row.location ? `• ${row.location}` : ''} {row.salary ? `• ${row.salary}` : ''}
- Saved on {new Date(row.saved_at).toLocaleString('en-IN')}
+ Saved on {renderSavedAt(row.saved_at)}
-
@@ -89,4 +124,3 @@ export default function JobSeekerSavedJobsPage() {
);
}
-
diff --git a/src/components/dashboard/MyDashboardPage.tsx b/src/components/dashboard/MyDashboardPage.tsx
index 964c9f6..7d24fd5 100644
--- a/src/components/dashboard/MyDashboardPage.tsx
+++ b/src/components/dashboard/MyDashboardPage.tsx
@@ -1,6 +1,7 @@
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
import { BTN_GHOST, CARD } from '~/components/DashboardShell';
import { PROFESSIONAL_ROLE_SET, ROLE_PREFIXES, type RoleKey } from './RoleDashboardShared';
+import { readJobSeekerProfile } from '~/lib/job-seeker-custom-data';
const API = '/api/gateway';
@@ -63,21 +64,38 @@ export default function MyDashboardPage(props: Props) {
);
if (!reqRes.ok) setErr('Some customer metrics could not be loaded.');
} else if (props.roleKey === 'JOB_SEEKER') {
- const [jobsRes, appsRes] = await Promise.all([
+ const [jobsRes, appsRes, profile] = await Promise.all([
apiFetch('/api/jobseeker/jobs?page=1&limit=100'),
apiFetch('/api/jobseeker/applications?page=1&limit=100'),
+ readJobSeekerProfile(),
]);
const jobsJson = await jobsRes.json().catch(() => ({}));
const appsJson = await appsRes.json().catch(() => ({}));
const jobs = Array.isArray(jobsJson?.data) ? jobsJson.data : [];
const apps = Array.isArray(appsJson?.data) ? appsJson.data : [];
+ const customData = (profile?.custom_data && typeof profile.custom_data === 'object')
+ ? (profile.custom_data as Record
)
+ : {};
+ const savedJobs = Array.isArray(customData.saved_jobs) ? customData.saved_jobs : [];
+ const portfolio = (customData.job_seeker_portfolio && typeof customData.job_seeker_portfolio === 'object')
+ ? (customData.job_seeker_portfolio as Record)
+ : {};
+ const profileStatus = String(profile?.status || 'NOT_SUBMITTED').replace(/_/g, ' ');
+ const portfolioDone = Boolean(
+ String(portfolio.headline || '').trim()
+ && String(portfolio.education || '').trim()
+ && String(portfolio.workExperience || '').trim()
+ && String(portfolio.skills || '').trim(),
+ );
next.push(
{ title: 'Available Jobs', value: String(jobs.length), hint: 'Open approved jobs' },
{ title: 'My Applications', value: String(apps.length), hint: 'Total applications submitted' },
{ title: 'Shortlisted', value: String(apps.filter((a: any) => String(a.status || '').toUpperCase() === 'SHORTLISTED').length), hint: 'Moved ahead in process' },
- { title: 'Under Review', value: String(apps.filter((a: any) => String(a.status || '').toUpperCase().includes('REVIEW')).length), hint: 'Awaiting decision' },
+ { title: 'Saved Jobs', value: String(savedJobs.length), hint: 'Bookmarked for later' },
+ { title: 'Profile Status', value: profileStatus, hint: 'Verification state' },
+ { title: 'Portfolio', value: portfolioDone ? 'Complete' : 'Incomplete', hint: 'Education/work/skills sections' },
);
- if (!jobsRes.ok && !appsRes.ok) setErr('Some job seeker metrics could not be loaded.');
+ if (!jobsRes.ok && !appsRes.ok && !profile) setErr('Some job seeker metrics could not be loaded.');
} else if (PROFESSIONAL_ROLE_SET.has(props.roleKey)) {
const prefix = ROLE_PREFIXES[props.roleKey];
const [marketRes, reqRes, walletRes] = await Promise.all([
@@ -159,4 +177,3 @@ export default function MyDashboardPage(props: Props) {
);
}
-
diff --git a/src/components/dashboard/PortfolioPage.tsx b/src/components/dashboard/PortfolioPage.tsx
index e2ff986..cca9567 100644
--- a/src/components/dashboard/PortfolioPage.tsx
+++ b/src/components/dashboard/PortfolioPage.tsx
@@ -1,10 +1,11 @@
/**
* PortfolioPage — real My Portfolio CRUD, wired to backend.
- * Uses existing /api/:rolePrefix/portfolio/me endpoints.
- * Professionals only.
+ * Uses existing /api/:rolePrefix/portfolio/me endpoints for professionals.
+ * Job seekers get a dedicated portfolio editor (education/work experience/skills).
*/
import { For, Show, createSignal, onMount } from 'solid-js';
import { CARD, BTN_ORANGE, BTN_GHOST, BTN_PRIMARY, INPUT, LABEL } from '~/components/DashboardShell';
+import { readJobSeekerProfile, updateJobSeekerCustomData } from '~/lib/job-seeker-custom-data';
const API = '/api/gateway';
@@ -39,7 +40,23 @@ interface FormState {
tags: string;
}
+interface JobSeekerPortfolioState {
+ headline: string;
+ summary: string;
+ education: string;
+ workExperience: string;
+ skills: string;
+}
+
const EMPTY_FORM: FormState = { title: '', description: '', tags: '' };
+const EMPTY_JOB_SEEKER_FORM: JobSeekerPortfolioState = {
+ headline: '',
+ summary: '',
+ education: '',
+ workExperience: '',
+ skills: '',
+};
+const JOB_SEEKER_FALLBACK_TABS = ['About', 'Education', 'Work Experience', 'Skills'];
// ── Helpers ───────────────────────────────────────────────────────────────────
@@ -55,11 +72,14 @@ async function apiFetch(path: string, opts?: RequestInit) {
interface Props {
roleKey: string;
+ runtimeTabs?: string[];
+ runtimeFields?: string[];
}
export default function PortfolioPage(props: Props) {
const prefix = () => ROLE_PREFIX[props.roleKey] ?? '';
const isProfessional = () => Boolean(prefix());
+ const isJobSeeker = () => String(props.roleKey || '').toUpperCase() === 'JOB_SEEKER';
const [items, setItems] = createSignal