All job seeker pages are already connected to real APIs:
- Jobs: /api/jobseeker/jobs (real company job postings)
- Applications: /api/jobseeker/applications (my applied jobs)
- Saved Jobs: Custom data storage for bookmarked jobs
- Apply: POST /api/jobseeker/jobs/{id}/apply
Dashboard shows real data from backend, not mock preview.
126 lines
4.7 KiB
TypeScript
126 lines
4.7 KiB
TypeScript
import { For, Show, createSignal, onMount } from 'solid-js';
|
|
import { BTN_GHOST, CARD } from '~/components/DashboardShell';
|
|
import { readJobSeekerProfile, updateJobSeekerCustomData } from '~/lib/job-seeker-custom-data';
|
|
|
|
type SavedJob = {
|
|
id: string;
|
|
title: string;
|
|
company?: string;
|
|
location?: string;
|
|
salary?: string;
|
|
saved_at: string;
|
|
};
|
|
|
|
function toSavedJobs(value: unknown): SavedJob[] {
|
|
if (!Array.isArray(value)) return [];
|
|
return value
|
|
.map((item) => {
|
|
const row = item as Record<string, unknown>;
|
|
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 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<SavedJob[]>([]);
|
|
const [loading, setLoading] = createSignal(true);
|
|
const [err, setErr] = createSignal('');
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
onMount(() => void loadRows());
|
|
|
|
const removeRow = async (id: string) => {
|
|
const next = rows().filter((row) => row.id !== id);
|
|
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 (
|
|
<div style={{ display: 'grid', gap: '14px', 'max-width': '980px' }}>
|
|
<div style={CARD}>
|
|
<p style={{ margin: '0', 'font-size': '22px', 'font-weight': '800', color: '#0D0D2A' }}>Saved Jobs</p>
|
|
<p style={{ margin: '4px 0 0', 'font-size': '13px', color: '#6B7280' }}>
|
|
Jobs bookmarked for later.
|
|
</p>
|
|
</div>
|
|
|
|
<Show when={err()}>
|
|
<div style={{ ...CARD, border: '1px solid #FECACA', background: '#FEF2F2', padding: '12px 14px', color: '#B91C1C', 'font-size': '13px', 'font-weight': '600' }}>{err()}</div>
|
|
</Show>
|
|
|
|
<div style={CARD}>
|
|
<div style={{ display: 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-bottom': '10px' }}>
|
|
<p style={{ margin: '0', 'font-size': '16px', 'font-weight': '700', color: '#111827' }}>Bookmarked Jobs</p>
|
|
<button type="button" onClick={() => void loadRows()} style={BTN_GHOST}>Refresh</button>
|
|
</div>
|
|
|
|
<Show when={loading()}>
|
|
<p style={{ margin: '0', color: '#9CA3AF', 'font-size': '13px' }}>Loading saved jobs...</p>
|
|
</Show>
|
|
<Show when={!loading() && rows().length === 0}>
|
|
<p style={{ margin: '0', color: '#6B7280', 'font-size': '13px' }}>No saved jobs yet.</p>
|
|
</Show>
|
|
<Show when={!loading() && rows().length > 0}>
|
|
<div style={{ display: 'grid', gap: '10px' }}>
|
|
<For each={rows()}>
|
|
{(row) => (
|
|
<div style={{ border: '1px solid #E5E7EB', 'border-radius': '12px', padding: '12px', background: '#FCFCFD' }}>
|
|
<div style={{ display: 'flex', 'justify-content': 'space-between', gap: '10px', 'flex-wrap': 'wrap' }}>
|
|
<div>
|
|
<p style={{ margin: '0', 'font-size': '14px', 'font-weight': '800', color: '#111827' }}>{row.title}</p>
|
|
<p style={{ margin: '4px 0 0', 'font-size': '12px', color: '#6B7280' }}>
|
|
{row.company || '—'} {row.location ? `• ${row.location}` : ''} {row.salary ? `• ${row.salary}` : ''}
|
|
</p>
|
|
<p style={{ margin: '4px 0 0', 'font-size': '12px', color: '#9CA3AF' }}>
|
|
Saved on {renderSavedAt(row.saved_at)}
|
|
</p>
|
|
</div>
|
|
<button type="button" onClick={() => void removeRow(row.id)} style={{ ...BTN_GHOST, height: '32px', 'font-size': '12px', padding: '0 12px' }}>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</div>
|
|
</Show>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|