2026-04-08 22:40:43 +02:00
|
|
|
import { For, Show, createSignal, onMount } from 'solid-js';
|
|
|
|
|
import { BTN_GHOST, CARD } from '~/components/DashboardShell';
|
|
|
|
|
|
|
|
|
|
const API = '/api/gateway';
|
|
|
|
|
|
|
|
|
|
type ApplicationItem = {
|
|
|
|
|
id: string;
|
|
|
|
|
job_id: string;
|
|
|
|
|
status: string;
|
|
|
|
|
applied_at?: string;
|
|
|
|
|
resume_url?: string | null;
|
|
|
|
|
cover_letter?: string | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
async function apiFetch(path: string, opts?: RequestInit) {
|
|
|
|
|
return fetch(`${API}${path}`, {
|
|
|
|
|
...opts,
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
headers: { 'Content-Type': 'application/json', ...(opts?.headers ?? {}) },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function JobSeekerApplicationsPage() {
|
|
|
|
|
const [rows, setRows] = createSignal<ApplicationItem[]>([]);
|
|
|
|
|
const [loading, setLoading] = createSignal(true);
|
|
|
|
|
const [statusFilter, setStatusFilter] = createSignal('');
|
|
|
|
|
const [err, setErr] = createSignal('');
|
|
|
|
|
|
|
|
|
|
const loadRows = async (status = statusFilter()) => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setErr('');
|
|
|
|
|
try {
|
|
|
|
|
const query = new URLSearchParams();
|
|
|
|
|
query.set('page', '1');
|
|
|
|
|
query.set('limit', '100');
|
|
|
|
|
if (status) query.set('status', status);
|
|
|
|
|
const res = await apiFetch(`/api/jobseeker/applications?${query.toString()}`);
|
|
|
|
|
const payload = await res.json().catch(() => ({}));
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
setErr(payload.error || payload.message || 'Failed to load applications.');
|
|
|
|
|
setRows([]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setRows(Array.isArray(payload?.data) ? payload.data : []);
|
|
|
|
|
} catch {
|
|
|
|
|
setErr('Network error while loading applications.');
|
|
|
|
|
setRows([]);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMount(() => loadRows());
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ display: 'grid', gap: '14px', 'max-width': '980px' }}>
|
|
|
|
|
<div style={CARD}>
|
2026-04-22 01:26:36 +02:00
|
|
|
<p style={{ margin: '0', 'font-size': '18px', 'font-weight': '800', color: '#111827' }}>My Applications</p>
|
2026-04-08 22:40:43 +02:00
|
|
|
<p style={{ margin: '4px 0 0', 'font-size': '13px', color: '#6B7280' }}>Track all jobs you applied for.</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': 'flex-end', gap: '10px', 'flex-wrap': 'wrap' }}>
|
|
|
|
|
<div>
|
|
|
|
|
<p style={{ margin: '0 0 6px', 'font-size': '12px', 'font-weight': '600', color: '#374151' }}>Status Filter</p>
|
|
|
|
|
<select
|
|
|
|
|
value={statusFilter()}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
const next = e.currentTarget.value;
|
|
|
|
|
setStatusFilter(next);
|
|
|
|
|
loadRows(next);
|
|
|
|
|
}}
|
|
|
|
|
style={{ height: '38px', 'border-radius': '8px', border: '1px solid #E5E7EB', padding: '0 10px', 'font-size': '13px' }}
|
|
|
|
|
>
|
|
|
|
|
<option value="">All</option>
|
|
|
|
|
<option value="APPLIED">Applied</option>
|
|
|
|
|
<option value="UNDER_REVIEW">Under Review</option>
|
|
|
|
|
<option value="SHORTLISTED">Shortlisted</option>
|
|
|
|
|
<option value="REJECTED">Rejected</option>
|
|
|
|
|
<option value="WITHDRAWN">Withdrawn</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<button type="button" onClick={() => loadRows()} style={BTN_GHOST}>Refresh</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style={CARD}>
|
|
|
|
|
<Show when={loading()}>
|
|
|
|
|
<p style={{ margin: '0', color: '#9CA3AF', 'font-size': '13px' }}>Loading applications...</p>
|
|
|
|
|
</Show>
|
|
|
|
|
<Show when={!loading() && rows().length === 0}>
|
|
|
|
|
<p style={{ margin: '0', color: '#6B7280', 'font-size': '13px' }}>No applications found.</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' }}>Application #{row.id.slice(0, 8)}</p>
|
|
|
|
|
<p style={{ margin: '4px 0 0', 'font-size': '12px', color: '#6B7280' }}>
|
|
|
|
|
Job: {row.job_id?.slice(0, 8) || '—'} {row.applied_at ? `• ${new Date(row.applied_at).toLocaleString('en-IN')}` : ''}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<span style={{ display: 'inline-flex', height: '24px', 'align-items': 'center', padding: '0 10px', 'border-radius': '999px', background: '#EEF2FF', color: '#3730A3', 'font-size': '11px', 'font-weight': '700' }}>
|
|
|
|
|
{String(row.status || 'APPLIED').replace(/_/g, ' ')}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<Show when={row.cover_letter}>
|
|
|
|
|
<p style={{ margin: '8px 0 0', 'font-size': '13px', color: '#374151' }}>{row.cover_letter}</p>
|
|
|
|
|
</Show>
|
|
|
|
|
<Show when={row.resume_url}>
|
|
|
|
|
<p style={{ margin: '8px 0 0', 'font-size': '12px' }}>
|
|
|
|
|
<a href={row.resume_url!} target="_blank" rel="noreferrer" style={{ color: '#1D4ED8', 'font-weight': '600' }}>
|
|
|
|
|
View Resume
|
|
|
|
|
</a>
|
|
|
|
|
</p>
|
|
|
|
|
</Show>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|